1 // 4.0.25 (2014-04-30) 2 3 /** 4 * Compiled inline version. (Library mode) 5 */ 6 7 /*jshint smarttabs:true, undef:true, latedef:true, curly:true, bitwise:true, camelcase:true */ 8 /*globals $code */ 9 10 (function(exports, undefined) { 11 "use strict"; 12 13 var modules = {}; 14 15 function require(ids, callback) { 16 var module, defs = []; 17 18 for (var i = 0; i < ids.length; ++i) { 19 module = modules[ids[i]] || resolve(ids[i]); 20 if (!module) { 21 throw 'module definition dependecy not found: ' + ids[i]; 22 } 23 24 defs.push(module); 25 } 26 27 callback.apply(null, defs); 28 } 29 30 function define(id, dependencies, definition) { 31 if (typeof id !== 'string') { 32 throw 'invalid module definition, module id must be defined and be a string'; 33 } 34 35 if (dependencies === undefined) { 36 throw 'invalid module definition, dependencies must be specified'; 37 } 38 39 if (definition === undefined) { 40 throw 'invalid module definition, definition function must be specified'; 41 } 42 43 require(dependencies, function() { 44 modules[id] = definition.apply(null, arguments); 45 }); 46 } 47 48 function defined(id) { 49 return !!modules[id]; 50 } 51 52 function resolve(id) { 53 var target = exports; 54 var fragments = id.split(/[.\/]/); 55 56 for (var fi = 0; fi < fragments.length; ++fi) { 57 if (!target[fragments[fi]]) { 58 return; 59 } 60 61 target = target[fragments[fi]]; 62 } 63 64 return target; 65 } 66 67 function expose(ids) { 68 for (var i = 0; i < ids.length; i++) { 69 var target = exports; 70 var id = ids[i]; 71 var fragments = id.split(/[.\/]/); 72 73 for (var fi = 0; fi < fragments.length - 1; ++fi) { 74 if (target[fragments[fi]] === undefined) { 75 target[fragments[fi]] = {}; 76 } 77 78 target = target[fragments[fi]]; 79 } 80 81 target[fragments[fragments.length - 1]] = modules[id]; 82 } 83 } 84 85 // Included from: js/tinymce/classes/dom/EventUtils.js 86 87 /** 88 * EventUtils.js 89 * 90 * Copyright, Moxiecode Systems AB 91 * Released under LGPL License. 92 * 93 * License: http://www.tinymce.com/license 94 * Contributing: http://www.tinymce.com/contributing 95 */ 96 97 /*jshint loopfunc:true*/ 98 /*eslint no-loop-func:0 */ 99 100 define("tinymce/dom/EventUtils", [], function() { 101 "use strict"; 102 103 var eventExpandoPrefix = "mce-data-"; 104 var mouseEventRe = /^(?:mouse|contextmenu)|click/; 105 var deprecated = {keyLocation: 1, layerX: 1, layerY: 1, returnValue: 1}; 106 107 /** 108 * Binds a native event to a callback on the speified target. 109 */ 110 function addEvent(target, name, callback, capture) { 111 if (target.addEventListener) { 112 target.addEventListener(name, callback, capture || false); 113 } else if (target.attachEvent) { 114 target.attachEvent('on' + name, callback); 115 } 116 } 117 118 /** 119 * Unbinds a native event callback on the specified target. 120 */ 121 function removeEvent(target, name, callback, capture) { 122 if (target.removeEventListener) { 123 target.removeEventListener(name, callback, capture || false); 124 } else if (target.detachEvent) { 125 target.detachEvent('on' + name, callback); 126 } 127 } 128 129 /** 130 * Normalizes a native event object or just adds the event specific methods on a custom event. 131 */ 132 function fix(originalEvent, data) { 133 var name, event = data || {}, undef; 134 135 // Dummy function that gets replaced on the delegation state functions 136 function returnFalse() { 137 return false; 138 } 139 140 // Dummy function that gets replaced on the delegation state functions 141 function returnTrue() { 142 return true; 143 } 144 145 // Copy all properties from the original event 146 for (name in originalEvent) { 147 // layerX/layerY is deprecated in Chrome and produces a warning 148 if (!deprecated[name]) { 149 event[name] = originalEvent[name]; 150 } 151 } 152 153 // Normalize target IE uses srcElement 154 if (!event.target) { 155 event.target = event.srcElement || document; 156 } 157 158 // Calculate pageX/Y if missing and clientX/Y available 159 if (originalEvent && mouseEventRe.test(originalEvent.type) && originalEvent.pageX === undef && originalEvent.clientX !== undef) { 160 var eventDoc = event.target.ownerDocument || document; 161 var doc = eventDoc.documentElement; 162 var body = eventDoc.body; 163 164 event.pageX = originalEvent.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - 165 ( doc && doc.clientLeft || body && body.clientLeft || 0); 166 167 event.pageY = originalEvent.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0 ) - 168 ( doc && doc.clientTop || body && body.clientTop || 0); 169 } 170 171 // Add preventDefault method 172 event.preventDefault = function() { 173 event.isDefaultPrevented = returnTrue; 174 175 // Execute preventDefault on the original event object 176 if (originalEvent) { 177 if (originalEvent.preventDefault) { 178 originalEvent.preventDefault(); 179 } else { 180 originalEvent.returnValue = false; // IE 181 } 182 } 183 }; 184 185 // Add stopPropagation 186 event.stopPropagation = function() { 187 event.isPropagationStopped = returnTrue; 188 189 // Execute stopPropagation on the original event object 190 if (originalEvent) { 191 if (originalEvent.stopPropagation) { 192 originalEvent.stopPropagation(); 193 } else { 194 originalEvent.cancelBubble = true; // IE 195 } 196 } 197 }; 198 199 // Add stopImmediatePropagation 200 event.stopImmediatePropagation = function() { 201 event.isImmediatePropagationStopped = returnTrue; 202 event.stopPropagation(); 203 }; 204 205 // Add event delegation states 206 if (!event.isDefaultPrevented) { 207 event.isDefaultPrevented = returnFalse; 208 event.isPropagationStopped = returnFalse; 209 event.isImmediatePropagationStopped = returnFalse; 210 } 211 212 return event; 213 } 214 215 /** 216 * Bind a DOMContentLoaded event across browsers and executes the callback once the page DOM is initialized. 217 * It will also set/check the domLoaded state of the event_utils instance so ready isn't called multiple times. 218 */ 219 function bindOnReady(win, callback, eventUtils) { 220 var doc = win.document, event = {type: 'ready'}; 221 222 if (eventUtils.domLoaded) { 223 callback(event); 224 return; 225 } 226 227 // Gets called when the DOM is ready 228 function readyHandler() { 229 if (!eventUtils.domLoaded) { 230 eventUtils.domLoaded = true; 231 callback(event); 232 } 233 } 234 235 function waitForDomLoaded() { 236 // Check complete or interactive state if there is a body 237 // element on some iframes IE 8 will produce a null body 238 if (doc.readyState === "complete" || (doc.readyState === "interactive" && doc.body)) { 239 removeEvent(doc, "readystatechange", waitForDomLoaded); 240 readyHandler(); 241 } 242 } 243 244 function tryScroll() { 245 try { 246 // If IE is used, use the trick by Diego Perini licensed under MIT by request to the author. 247 // http://javascript.nwbox.com/IEContentLoaded/ 248 doc.documentElement.doScroll("left"); 249 } catch (ex) { 250 setTimeout(tryScroll, 0); 251 return; 252 } 253 254 readyHandler(); 255 } 256 257 // Use W3C method 258 if (doc.addEventListener) { 259 if (doc.readyState === "complete") { 260 readyHandler(); 261 } else { 262 addEvent(win, 'DOMContentLoaded', readyHandler); 263 } 264 } else { 265 // Use IE method 266 addEvent(doc, "readystatechange", waitForDomLoaded); 267 268 // Wait until we can scroll, when we can the DOM is initialized 269 if (doc.documentElement.doScroll && win.self === win.top) { 270 tryScroll(); 271 } 272 } 273 274 // Fallback if any of the above methods should fail for some odd reason 275 addEvent(win, 'load', readyHandler); 276 } 277 278 /** 279 * This class enables you to bind/unbind native events to elements and normalize it's behavior across browsers. 280 */ 281 function EventUtils() { 282 var self = this, events = {}, count, expando, hasFocusIn, hasMouseEnterLeave, mouseEnterLeave; 283 284 expando = eventExpandoPrefix + (+new Date()).toString(32); 285 hasMouseEnterLeave = "onmouseenter" in document.documentElement; 286 hasFocusIn = "onfocusin" in document.documentElement; 287 mouseEnterLeave = {mouseenter: 'mouseover', mouseleave: 'mouseout'}; 288 count = 1; 289 290 // State if the DOMContentLoaded was executed or not 291 self.domLoaded = false; 292 self.events = events; 293 294 /** 295 * Executes all event handler callbacks for a specific event. 296 * 297 * @private 298 * @param {Event} evt Event object. 299 * @param {String} id Expando id value to look for. 300 */ 301 function executeHandlers(evt, id) { 302 var callbackList, i, l, callback, container = events[id]; 303 304 callbackList = container && container[evt.type]; 305 if (callbackList) { 306 for (i = 0, l = callbackList.length; i < l; i++) { 307 callback = callbackList[i]; 308 309 // Check if callback exists might be removed if a unbind is called inside the callback 310 if (callback && callback.func.call(callback.scope, evt) === false) { 311 evt.preventDefault(); 312 } 313 314 // Should we stop propagation to immediate listeners 315 if (evt.isImmediatePropagationStopped()) { 316 return; 317 } 318 } 319 } 320 } 321 322 /** 323 * Binds a callback to an event on the specified target. 324 * 325 * @method bind 326 * @param {Object} target Target node/window or custom object. 327 * @param {String} names Name of the event to bind. 328 * @param {function} callback Callback function to execute when the event occurs. 329 * @param {Object} scope Scope to call the callback function on, defaults to target. 330 * @return {function} Callback function that got bound. 331 */ 332 self.bind = function(target, names, callback, scope) { 333 var id, callbackList, i, name, fakeName, nativeHandler, capture, win = window; 334 335 // Native event handler function patches the event and executes the callbacks for the expando 336 function defaultNativeHandler(evt) { 337 executeHandlers(fix(evt || win.event), id); 338 } 339 340 // Don't bind to text nodes or comments 341 if (!target || target.nodeType === 3 || target.nodeType === 8) { 342 return; 343 } 344 345 // Create or get events id for the target 346 if (!target[expando]) { 347 id = count++; 348 target[expando] = id; 349 events[id] = {}; 350 } else { 351 id = target[expando]; 352 } 353 354 // Setup the specified scope or use the target as a default 355 scope = scope || target; 356 357 // Split names and bind each event, enables you to bind multiple events with one call 358 names = names.split(' '); 359 i = names.length; 360 while (i--) { 361 name = names[i]; 362 nativeHandler = defaultNativeHandler; 363 fakeName = capture = false; 364 365 // Use ready instead of DOMContentLoaded 366 if (name === "DOMContentLoaded") { 367 name = "ready"; 368 } 369 370 // DOM is already ready 371 if (self.domLoaded && name === "ready" && target.readyState == 'complete') { 372 callback.call(scope, fix({type: name})); 373 continue; 374 } 375 376 // Handle mouseenter/mouseleaver 377 if (!hasMouseEnterLeave) { 378 fakeName = mouseEnterLeave[name]; 379 380 if (fakeName) { 381 nativeHandler = function(evt) { 382 var current, related; 383 384 current = evt.currentTarget; 385 related = evt.relatedTarget; 386 387 // Check if related is inside the current target if it's not then the event should 388 // be ignored since it's a mouseover/mouseout inside the element 389 if (related && current.contains) { 390 // Use contains for performance 391 related = current.contains(related); 392 } else { 393 while (related && related !== current) { 394 related = related.parentNode; 395 } 396 } 397 398 // Fire fake event 399 if (!related) { 400 evt = fix(evt || win.event); 401 evt.type = evt.type === 'mouseout' ? 'mouseleave' : 'mouseenter'; 402 evt.target = current; 403 executeHandlers(evt, id); 404 } 405 }; 406 } 407 } 408 409 // Fake bubbeling of focusin/focusout 410 if (!hasFocusIn && (name === "focusin" || name === "focusout")) { 411 capture = true; 412 fakeName = name === "focusin" ? "focus" : "blur"; 413 nativeHandler = function(evt) { 414 evt = fix(evt || win.event); 415 evt.type = evt.type === 'focus' ? 'focusin' : 'focusout'; 416 executeHandlers(evt, id); 417 }; 418 } 419 420 // Setup callback list and bind native event 421 callbackList = events[id][name]; 422 if (!callbackList) { 423 events[id][name] = callbackList = [{func: callback, scope: scope}]; 424 callbackList.fakeName = fakeName; 425 callbackList.capture = capture; 426 427 // Add the nativeHandler to the callback list so that we can later unbind it 428 callbackList.nativeHandler = nativeHandler; 429 430 // Check if the target has native events support 431 432 if (name === "ready") { 433 bindOnReady(target, nativeHandler, self); 434 } else { 435 addEvent(target, fakeName || name, nativeHandler, capture); 436 } 437 } else { 438 if (name === "ready" && self.domLoaded) { 439 callback({type: name}); 440 } else { 441 // If it already has an native handler then just push the callback 442 callbackList.push({func: callback, scope: scope}); 443 } 444 } 445 } 446 447 target = callbackList = 0; // Clean memory for IE 448 449 return callback; 450 }; 451 452 /** 453 * Unbinds the specified event by name, name and callback or all events on the target. 454 * 455 * @method unbind 456 * @param {Object} target Target node/window or custom object. 457 * @param {String} names Optional event name to unbind. 458 * @param {function} callback Optional callback function to unbind. 459 * @return {EventUtils} Event utils instance. 460 */ 461 self.unbind = function(target, names, callback) { 462 var id, callbackList, i, ci, name, eventMap; 463 464 // Don't bind to text nodes or comments 465 if (!target || target.nodeType === 3 || target.nodeType === 8) { 466 return self; 467 } 468 469 // Unbind event or events if the target has the expando 470 id = target[expando]; 471 if (id) { 472 eventMap = events[id]; 473 474 // Specific callback 475 if (names) { 476 names = names.split(' '); 477 i = names.length; 478 while (i--) { 479 name = names[i]; 480 callbackList = eventMap[name]; 481 482 // Unbind the event if it exists in the map 483 if (callbackList) { 484 // Remove specified callback 485 if (callback) { 486 ci = callbackList.length; 487 while (ci--) { 488 if (callbackList[ci].func === callback) { 489 var nativeHandler = callbackList.nativeHandler; 490 var fakeName = callbackList.fakeName, capture = callbackList.capture; 491 492 // Clone callbackList since unbind inside a callback would otherwise break the handlers loop 493 callbackList = callbackList.slice(0, ci).concat(callbackList.slice(ci + 1)); 494 callbackList.nativeHandler = nativeHandler; 495 callbackList.fakeName = fakeName; 496 callbackList.capture = capture; 497 498 eventMap[name] = callbackList; 499 } 500 } 501 } 502 503 // Remove all callbacks if there isn't a specified callback or there is no callbacks left 504 if (!callback || callbackList.length === 0) { 505 delete eventMap[name]; 506 removeEvent(target, callbackList.fakeName || name, callbackList.nativeHandler, callbackList.capture); 507 } 508 } 509 } 510 } else { 511 // All events for a specific element 512 for (name in eventMap) { 513 callbackList = eventMap[name]; 514 removeEvent(target, callbackList.fakeName || name, callbackList.nativeHandler, callbackList.capture); 515 } 516 517 eventMap = {}; 518 } 519 520 // Check if object is empty, if it isn't then we won't remove the expando map 521 for (name in eventMap) { 522 return self; 523 } 524 525 // Delete event object 526 delete events[id]; 527 528 // Remove expando from target 529 try { 530 // IE will fail here since it can't delete properties from window 531 delete target[expando]; 532 } catch (ex) { 533 // IE will set it to null 534 target[expando] = null; 535 } 536 } 537 538 return self; 539 }; 540 541 /** 542 * Fires the specified event on the specified target. 543 * 544 * @method fire 545 * @param {Object} target Target node/window or custom object. 546 * @param {String} name Event name to fire. 547 * @param {Object} args Optional arguments to send to the observers. 548 * @return {EventUtils} Event utils instance. 549 */ 550 self.fire = function(target, name, args) { 551 var id; 552 553 // Don't bind to text nodes or comments 554 if (!target || target.nodeType === 3 || target.nodeType === 8) { 555 return self; 556 } 557 558 // Build event object by patching the args 559 args = fix(null, args); 560 args.type = name; 561 args.target = target; 562 563 do { 564 // Found an expando that means there is listeners to execute 565 id = target[expando]; 566 if (id) { 567 executeHandlers(args, id); 568 } 569 570 // Walk up the DOM 571 target = target.parentNode || target.ownerDocument || target.defaultView || target.parentWindow; 572 } while (target && !args.isPropagationStopped()); 573 574 return self; 575 }; 576 577 /** 578 * Removes all bound event listeners for the specified target. This will also remove any bound 579 * listeners to child nodes within that target. 580 * 581 * @method clean 582 * @param {Object} target Target node/window object. 583 * @return {EventUtils} Event utils instance. 584 */ 585 self.clean = function(target) { 586 var i, children, unbind = self.unbind; 587 588 // Don't bind to text nodes or comments 589 if (!target || target.nodeType === 3 || target.nodeType === 8) { 590 return self; 591 } 592 593 // Unbind any element on the specificed target 594 if (target[expando]) { 595 unbind(target); 596 } 597 598 // Target doesn't have getElementsByTagName it's probably a window object then use it's document to find the children 599 if (!target.getElementsByTagName) { 600 target = target.document; 601 } 602 603 // Remove events from each child element 604 if (target && target.getElementsByTagName) { 605 unbind(target); 606 607 children = target.getElementsByTagName('*'); 608 i = children.length; 609 while (i--) { 610 target = children[i]; 611 612 if (target[expando]) { 613 unbind(target); 614 } 615 } 616 } 617 618 return self; 619 }; 620 621 /** 622 * Destroys the event object. Call this on IE to remove memory leaks. 623 */ 624 self.destroy = function() { 625 events = {}; 626 }; 627 628 // Legacy function for canceling events 629 self.cancel = function(e) { 630 if (e) { 631 e.preventDefault(); 632 e.stopImmediatePropagation(); 633 } 634 635 return false; 636 }; 637 } 638 639 EventUtils.Event = new EventUtils(); 640 EventUtils.Event.bind(window, 'ready', function() {}); 641 642 return EventUtils; 643 }); 644 645 // Included from: js/tinymce/classes/dom/Sizzle.js 646 647 /** 648 * Sizzle.js 649 * 650 * Copyright, Moxiecode Systems AB 651 * Released under LGPL License. 652 * 653 * License: http://www.tinymce.com/license 654 * Contributing: http://www.tinymce.com/contributing 655 * 656 * @ignore-file 657 */ 658 659 /*jshint bitwise:false, expr:true, noempty:false, sub:true, eqnull:true, latedef:false, maxlen:255 */ 660 /*eslint dot-notation:0, no-empty:0, no-cond-assign:0, no-unused-expressions:0, new-cap:0, no-nested-ternary:0, func-style:0, no-bitwise: 0 */ 661 662 /* 663 * Sizzle CSS Selector Engine 664 * Copyright, The Dojo Foundation 665 * Released under the MIT, BSD, and GPL Licenses. 666 * More information: http://sizzlejs.com/ 667 */ 668 define("tinymce/dom/Sizzle", [], function() { 669 var i, 670 cachedruns, 671 Expr, 672 getText, 673 isXML, 674 compile, 675 outermostContext, 676 recompare, 677 sortInput, 678 679 // Local document vars 680 setDocument, 681 document, 682 docElem, 683 documentIsHTML, 684 rbuggyQSA, 685 rbuggyMatches, 686 matches, 687 contains, 688 689 // Instance-specific data 690 expando = "sizzle" + -(new Date()), 691 preferredDoc = window.document, 692 support = {}, 693 dirruns = 0, 694 done = 0, 695 classCache = createCache(), 696 tokenCache = createCache(), 697 compilerCache = createCache(), 698 hasDuplicate = false, 699 sortOrder = function() { return 0; }, 700 701 // General-purpose constants 702 strundefined = typeof undefined, 703 MAX_NEGATIVE = 1 << 31, 704 705 // Array methods 706 arr = [], 707 pop = arr.pop, 708 push_native = arr.push, 709 push = arr.push, 710 slice = arr.slice, 711 // Use a stripped-down indexOf if we can't use a native one 712 indexOf = arr.indexOf || function( elem ) { 713 var i = 0, 714 len = this.length; 715 for ( ; i < len; i++ ) { 716 if ( this[i] === elem ) { 717 return i; 718 } 719 } 720 return -1; 721 }, 722 723 724 // Regular expressions 725 726 // Whitespace characters http://www.w3.org/TR/css3-selectors/#whitespace 727 whitespace = "[\\x20\\t\\r\\n\\f]", 728 // http://www.w3.org/TR/css3-syntax/#characters 729 characterEncoding = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+", 730 731 // Loosely modeled on CSS identifier characters 732 // An unquoted value should be a CSS identifier http://www.w3.org/TR/css3-selectors/#attribute-selectors 733 // Proper syntax: http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier 734 identifier = characterEncoding.replace( "w", "w#" ), 735 736 // Acceptable operators http://www.w3.org/TR/selectors/#attribute-selectors 737 operators = "([*^$|!~]?=)", 738 attributes = "\\[" + whitespace + "*(" + characterEncoding + ")" + whitespace + 739 "*(?:" + operators + whitespace + "*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|(" + identifier + ")|)|)" + whitespace + "*\\]", 740 741 // Prefer arguments quoted, 742 // then not containing pseudos/brackets, 743 // then attribute selectors/non-parenthetical expressions, 744 // then anything else 745 // These preferences are here to reduce the number of selectors 746 // needing tokenize in the PSEUDO preFilter 747 pseudos = ":(" + characterEncoding + ")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|" + attributes.replace( 3, 8 ) + ")*)|.*)\\)|)", 748 749 // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter 750 rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ), 751 752 rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), 753 rcombinators = new RegExp( "^" + whitespace + "*([\\x20\\t\\r\\n\\f>+~])" + whitespace + "*" ), 754 rpseudo = new RegExp( pseudos ), 755 ridentifier = new RegExp( "^" + identifier + "$" ), 756 757 matchExpr = { 758 "ID": new RegExp( "^#(" + characterEncoding + ")" ), 759 "CLASS": new RegExp( "^\\.(" + characterEncoding + ")" ), 760 "NAME": new RegExp( "^\\[name=['\"]?(" + characterEncoding + ")['\"]?\\]" ), 761 "TAG": new RegExp( "^(" + characterEncoding.replace( "w", "w*" ) + ")" ), 762 "ATTR": new RegExp( "^" + attributes ), 763 "PSEUDO": new RegExp( "^" + pseudos ), 764 "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace + 765 "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace + 766 "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), 767 // For use in libraries implementing .is() 768 // We use this for POS matching in `select` 769 "needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + 770 whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) 771 }, 772 773 rsibling = /[\x20\t\r\n\f]*[+~]/, 774 775 rnative = /^[^{]+\{\s*\[native code/, 776 777 // Easily-parseable/retrievable ID or TAG or CLASS selectors 778 rquickExpr = /^(?:#([\w\-]+)|(\w+)|\.([\w\-]+))$/, 779 780 rinputs = /^(?:input|select|textarea|button)$/i, 781 rheader = /^h\d$/i, 782 783 rescape = /'|\\/g, 784 rattributeQuotes = /\=[\x20\t\r\n\f]*([^'"\]]*)[\x20\t\r\n\f]*\]/g, 785 786 // CSS escapes http://www.w3.org/TR/CSS21/syndata.html#escaped-characters 787 runescape = /\\([\da-fA-F]{1,6}[\x20\t\r\n\f]?|.)/g, 788 funescape = function( _, escaped ) { 789 var high = "0x" + escaped - 0x10000; 790 // NaN means non-codepoint 791 return high !== high ? 792 escaped : 793 // BMP codepoint 794 high < 0 ? 795 String.fromCharCode( high + 0x10000 ) : 796 // Supplemental Plane codepoint (surrogate pair) 797 String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); 798 }; 799 800 // Optimize for push.apply( _, NodeList ) 801 try { 802 push.apply( 803 (arr = slice.call( preferredDoc.childNodes )), 804 preferredDoc.childNodes 805 ); 806 // Support: Android<4.0 807 // Detect silently failing push.apply 808 arr[ preferredDoc.childNodes.length ].nodeType; 809 } catch ( e ) { 810 push = { apply: arr.length ? 811 812 // Leverage slice if possible 813 function( target, els ) { 814 push_native.apply( target, slice.call(els) ); 815 } : 816 817 // Support: IE<9 818 // Otherwise append directly 819 function( target, els ) { 820 var j = target.length, 821 i = 0; 822 // Can't trust NodeList.length 823 while ( (target[j++] = els[i++]) ) {} 824 target.length = j - 1; 825 } 826 }; 827 } 828 829 /** 830 * For feature detection 831 * @param {Function} fn The function to test for native support 832 */ 833 function isNative( fn ) { 834 return rnative.test( fn + "" ); 835 } 836 837 /** 838 * Create key-value caches of limited size 839 * @returns {Function(string, Object)} Returns the Object data after storing it on itself with 840 * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) 841 * deleting the oldest entry 842 */ 843 function createCache() { 844 var cache, 845 keys = []; 846 847 cache = function( key, value ) { 848 // Use (key + " ") to avoid collision with native prototype properties (see Issue #157) 849 if ( keys.push( key += " " ) > Expr.cacheLength ) { 850 // Only keep the most recent entries 851 delete cache[ keys.shift() ]; 852 } 853 cache[ key ] = value; 854 return value; 855 }; 856 857 return cache; 858 } 859 860 /** 861 * Mark a function for special use by Sizzle 862 * @param {Function} fn The function to mark 863 */ 864 function markFunction( fn ) { 865 fn[ expando ] = true; 866 return fn; 867 } 868 869 /** 870 * Support testing using an element 871 * @param {Function} fn Passed the created div and expects a boolean result 872 */ 873 function assert( fn ) { 874 var div = document.createElement("div"); 875 876 try { 877 return !!fn( div ); 878 } catch (e) { 879 return false; 880 } finally { 881 // release memory in IE 882 div = null; 883 } 884 } 885 886 function Sizzle( selector, context, results, seed ) { 887 var match, elem, m, nodeType, 888 // QSA vars 889 i, groups, old, nid, newContext, newSelector; 890 891 if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) { 892 setDocument( context ); 893 } 894 895 context = context || document; 896 results = results || []; 897 898 if ( !selector || typeof selector !== "string" ) { 899 return results; 900 } 901 902 if ( (nodeType = context.nodeType) !== 1 && nodeType !== 9 ) { 903 return []; 904 } 905 906 if ( documentIsHTML && !seed ) { 907 908 // Shortcuts 909 if ( (match = rquickExpr.exec( selector )) ) { 910 // Speed-up: Sizzle("#ID") 911 if ( (m = match[1]) ) { 912 if ( nodeType === 9 ) { 913 elem = context.getElementById( m ); 914 // Check parentNode to catch when Blackberry 4.6 returns 915 // nodes that are no longer in the document #6963 916 if ( elem && elem.parentNode ) { 917 // Handle the case where IE, Opera, and Webkit return items 918 // by name instead of ID 919 if ( elem.id === m ) { 920 results.push( elem ); 921 return results; 922 } 923 } else { 924 return results; 925 } 926 } else { 927 // Context is not a document 928 if ( context.ownerDocument && (elem = context.ownerDocument.getElementById( m )) && 929 contains( context, elem ) && elem.id === m ) { 930 results.push( elem ); 931 return results; 932 } 933 } 934 935 // Speed-up: Sizzle("TAG") 936 } else if ( match[2] ) { 937 push.apply( results, context.getElementsByTagName( selector ) ); 938 return results; 939 940 // Speed-up: Sizzle(".CLASS") 941 } else if ( (m = match[3]) && support.getElementsByClassName && context.getElementsByClassName ) { 942 push.apply( results, context.getElementsByClassName( m ) ); 943 return results; 944 } 945 } 946 947 // QSA path 948 if ( support.qsa && !rbuggyQSA.test(selector) ) { 949 old = true; 950 nid = expando; 951 newContext = context; 952 newSelector = nodeType === 9 && selector; 953 954 // qSA works strangely on Element-rooted queries 955 // We can work around this by specifying an extra ID on the root 956 // and working up from there (Thanks to Andrew Dupont for the technique) 957 // IE 8 doesn't work on object elements 958 if ( nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) { 959 groups = tokenize( selector ); 960 961 if ( (old = context.getAttribute("id")) ) { 962 nid = old.replace( rescape, "\\$&" ); 963 } else { 964 context.setAttribute( "id", nid ); 965 } 966 nid = "[id='" + nid + "'] "; 967 968 i = groups.length; 969 while ( i-- ) { 970 groups[i] = nid + toSelector( groups[i] ); 971 } 972 newContext = rsibling.test( selector ) && context.parentNode || context; 973 newSelector = groups.join(","); 974 } 975 976 if ( newSelector ) { 977 try { 978 push.apply( results, 979 newContext.querySelectorAll( newSelector ) 980 ); 981 return results; 982 } catch(qsaError) { 983 } finally { 984 if ( !old ) { 985 context.removeAttribute("id"); 986 } 987 } 988 } 989 } 990 } 991 992 // All others 993 return select( selector.replace( rtrim, "$1" ), context, results, seed ); 994 } 995 996 /** 997 * Detect xml 998 * @param {Element|Object} elem An element or a document 999 */ 1000 isXML = Sizzle.isXML = function( elem ) { 1001 // documentElement is verified for cases where it doesn't yet exist 1002 // (such as loading iframes in IE - #4833) 1003 var documentElement = elem && (elem.ownerDocument || elem).documentElement; 1004 return documentElement ? documentElement.nodeName !== "HTML" : false; 1005 }; 1006 1007 /** 1008 * Sets document-related variables once based on the current document 1009 * @param {Element|Object} [doc] An element or document object to use to set the document 1010 * @returns {Object} Returns the current document 1011 */ 1012 setDocument = Sizzle.setDocument = function( node ) { 1013 var doc = node ? node.ownerDocument || node : preferredDoc; 1014 1015 // If no document and documentElement is available, return 1016 if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) { 1017 return document; 1018 } 1019 1020 // Set our document 1021 document = doc; 1022 docElem = doc.documentElement; 1023 1024 // Support tests 1025 documentIsHTML = !isXML( doc ); 1026 1027 // Check if getElementsByTagName("*") returns only elements 1028 support.getElementsByTagName = assert(function( div ) { 1029 div.appendChild( doc.createComment("") ); 1030 return !div.getElementsByTagName("*").length; 1031 }); 1032 1033 // Check if attributes should be retrieved by attribute nodes 1034 support.attributes = assert(function( div ) { 1035 div.innerHTML = "<select></select>"; 1036 var type = typeof div.lastChild.getAttribute("multiple"); 1037 // IE8 returns a string for some attributes even when not present 1038 return type !== "boolean" && type !== "string"; 1039 }); 1040 1041 // Check if getElementsByClassName can be trusted 1042 support.getElementsByClassName = assert(function( div ) { 1043 // Opera can't find a second classname (in 9.6) 1044 div.innerHTML = "<div class='hidden e'></div><div class='hidden'></div>"; 1045 if ( !div.getElementsByClassName || !div.getElementsByClassName("e").length ) { 1046 return false; 1047 } 1048 1049 // Safari 3.2 caches class attributes and doesn't catch changes 1050 div.lastChild.className = "e"; 1051 return div.getElementsByClassName("e").length === 2; 1052 }); 1053 1054 // Check if getElementsByName privileges form controls or returns elements by ID 1055 // If so, assume (for broader support) that getElementById returns elements by name 1056 support.getByName = assert(function( div ) { 1057 // Inject content 1058 div.id = expando + 0; 1059 // Support: Windows 8 Native Apps 1060 // Assigning innerHTML with "name" attributes throws uncatchable exceptions 1061 // http://msdn.microsoft.com/en-us/library/ie/hh465388.aspx 1062 div.appendChild( document.createElement("a") ).setAttribute( "name", expando ); 1063 div.appendChild( document.createElement("i") ).setAttribute( "name", expando ); 1064 docElem.appendChild( div ); 1065 1066 // Test 1067 var pass = doc.getElementsByName && 1068 // buggy browsers will return fewer than the correct 2 1069 doc.getElementsByName( expando ).length === 2 + 1070 // buggy browsers will return more than the correct 0 1071 doc.getElementsByName( expando + 0 ).length; 1072 1073 // Cleanup 1074 docElem.removeChild( div ); 1075 1076 return pass; 1077 }); 1078 1079 // Support: Webkit<537.32 1080 // Detached nodes confoundingly follow *each other* 1081 support.sortDetached = assert(function( div1 ) { 1082 return div1.compareDocumentPosition && 1083 // Should return 1, but Webkit returns 4 (following) 1084 (div1.compareDocumentPosition( document.createElement("div") ) & 1); 1085 }); 1086 1087 // IE6/7 return modified attributes 1088 Expr.attrHandle = assert(function( div ) { 1089 div.innerHTML = "<a href='#'></a>"; 1090 return div.firstChild && typeof div.firstChild.getAttribute !== strundefined && 1091 div.firstChild.getAttribute("href") === "#"; 1092 }) ? 1093 {} : 1094 { 1095 "href": function( elem ) { 1096 return elem.getAttribute( "href", 2 ); 1097 }, 1098 "type": function( elem ) { 1099 return elem.getAttribute("type"); 1100 } 1101 }; 1102 1103 // ID find and filter 1104 if ( support.getByName ) { 1105 Expr.find["ID"] = function( id, context ) { 1106 if ( typeof context.getElementById !== strundefined && documentIsHTML ) { 1107 var m = context.getElementById( id ); 1108 // Check parentNode to catch when Blackberry 4.6 returns 1109 // nodes that are no longer in the document #6963 1110 return m && m.parentNode ? [m] : []; 1111 } 1112 }; 1113 Expr.filter["ID"] = function( id ) { 1114 var attrId = id.replace( runescape, funescape ); 1115 return function( elem ) { 1116 return elem.getAttribute("id") === attrId; 1117 }; 1118 }; 1119 } else { 1120 Expr.find["ID"] = function( id, context ) { 1121 if ( typeof context.getElementById !== strundefined && documentIsHTML ) { 1122 var m = context.getElementById( id ); 1123 1124 return m ? 1125 m.id === id || typeof m.getAttributeNode !== strundefined && m.getAttributeNode("id").value === id ? 1126 [m] : 1127 undefined : 1128 []; 1129 } 1130 }; 1131 Expr.filter["ID"] = function( id ) { 1132 var attrId = id.replace( runescape, funescape ); 1133 return function( elem ) { 1134 var node = typeof elem.getAttributeNode !== strundefined && elem.getAttributeNode("id"); 1135 return node && node.value === attrId; 1136 }; 1137 }; 1138 } 1139 1140 // Tag 1141 Expr.find["TAG"] = support.getElementsByTagName ? 1142 function( tag, context ) { 1143 if ( typeof context.getElementsByTagName !== strundefined ) { 1144 return context.getElementsByTagName( tag ); 1145 } 1146 } : 1147 function( tag, context ) { 1148 var elem, 1149 tmp = [], 1150 i = 0, 1151 results = context.getElementsByTagName( tag ); 1152 1153 // Filter out possible comments 1154 if ( tag === "*" ) { 1155 while ( (elem = results[i++]) ) { 1156 if ( elem.nodeType === 1 ) { 1157 tmp.push( elem ); 1158 } 1159 } 1160 1161 return tmp; 1162 } 1163 return results; 1164 }; 1165 1166 // Name 1167 Expr.find["NAME"] = support.getByName && function( tag, context ) { 1168 if ( typeof context.getElementsByName !== strundefined ) { 1169 return context.getElementsByName( name ); 1170 } 1171 }; 1172 1173 // Class 1174 Expr.find["CLASS"] = support.getElementsByClassName && function( className, context ) { 1175 if ( typeof context.getElementsByClassName !== strundefined && documentIsHTML ) { 1176 return context.getElementsByClassName( className ); 1177 } 1178 }; 1179 1180 // QSA and matchesSelector support 1181 1182 // matchesSelector(:active) reports false when true (IE9/Opera 11.5) 1183 rbuggyMatches = []; 1184 1185 // qSa(:focus) reports false when true (Chrome 21), 1186 // no need to also add to buggyMatches since matches checks buggyQSA 1187 // A support test would require too much code (would include document ready) 1188 rbuggyQSA = [ ":focus" ]; 1189 1190 if ( (support.qsa = isNative(doc.querySelectorAll)) ) { 1191 // Build QSA regex 1192 // Regex strategy adopted from Diego Perini 1193 assert(function( div ) { 1194 // Select is set to empty string on purpose 1195 // This is to test IE's treatment of not explicitly 1196 // setting a boolean content attribute, 1197 // since its presence should be enough 1198 // http://bugs.jquery.com/ticket/12359 1199 div.innerHTML = "<select><option selected=''></option></select>"; 1200 1201 // IE8 - Some boolean attributes are not treated correctly 1202 if ( !div.querySelectorAll("[selected]").length ) { 1203 rbuggyQSA.push( "\\[" + whitespace + "*(?:checked|disabled|ismap|multiple|readonly|selected|value)" ); 1204 } 1205 1206 // Webkit/Opera - :checked should return selected option elements 1207 // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked 1208 // IE8 throws error here and will not see later tests 1209 if ( !div.querySelectorAll(":checked").length ) { 1210 rbuggyQSA.push(":checked"); 1211 } 1212 }); 1213 1214 assert(function( div ) { 1215 1216 // Opera 10-12/IE8 - ^= $= *= and empty values 1217 // Should not select anything 1218 div.innerHTML = "<input type='hidden' i=''/>"; 1219 if ( div.querySelectorAll("[i^='']").length ) { 1220 rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:\"\"|'')" ); 1221 } 1222 1223 // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled) 1224 // IE8 throws error here and will not see later tests 1225 if ( !div.querySelectorAll(":enabled").length ) { 1226 rbuggyQSA.push( ":enabled", ":disabled" ); 1227 } 1228 1229 // Opera 10-11 does not throw on post-comma invalid pseudos 1230 div.querySelectorAll("*,:x"); 1231 rbuggyQSA.push(",.*:"); 1232 }); 1233 } 1234 1235 if ( (support.matchesSelector = isNative( (matches = docElem.matchesSelector || 1236 docElem.mozMatchesSelector || 1237 docElem.webkitMatchesSelector || 1238 docElem.oMatchesSelector || 1239 docElem.msMatchesSelector) )) ) { 1240 1241 assert(function( div ) { 1242 // Check to see if it's possible to do matchesSelector 1243 // on a disconnected node (IE 9) 1244 support.disconnectedMatch = matches.call( div, "div" ); 1245 1246 // This should fail with an exception 1247 // Gecko does not error, returns false instead 1248 matches.call( div, "[s!='']:x" ); 1249 rbuggyMatches.push( "!=", pseudos ); 1250 }); 1251 } 1252 1253 rbuggyQSA = new RegExp( rbuggyQSA.join("|") ); 1254 rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join("|") ); 1255 1256 // Element contains another 1257 // Purposefully does not implement inclusive descendant 1258 // As in, an element does not contain itself 1259 contains = isNative(docElem.contains) || docElem.compareDocumentPosition ? 1260 function( a, b ) { 1261 var adown = a.nodeType === 9 ? a.documentElement : a, 1262 bup = b && b.parentNode; 1263 return a === bup || !!( bup && bup.nodeType === 1 && ( 1264 adown.contains ? 1265 adown.contains( bup ) : 1266 a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 1267 )); 1268 } : 1269 function( a, b ) { 1270 if ( b ) { 1271 while ( (b = b.parentNode) ) { 1272 if ( b === a ) { 1273 return true; 1274 } 1275 } 1276 } 1277 return false; 1278 }; 1279 1280 // Document order sorting 1281 sortOrder = docElem.compareDocumentPosition ? 1282 function( a, b ) { 1283 1284 // Flag for duplicate removal 1285 if ( a === b ) { 1286 hasDuplicate = true; 1287 return 0; 1288 } 1289 1290 var compare = b.compareDocumentPosition && a.compareDocumentPosition && a.compareDocumentPosition( b ); 1291 1292 if ( compare ) { 1293 // Disconnected nodes 1294 if ( compare & 1 || 1295 (recompare && b.compareDocumentPosition( a ) === compare) ) { 1296 1297 // Choose the first element that is related to our preferred document 1298 if ( a === doc || contains(preferredDoc, a) ) { 1299 return -1; 1300 } 1301 if ( b === doc || contains(preferredDoc, b) ) { 1302 return 1; 1303 } 1304 1305 // Maintain original order 1306 return sortInput ? 1307 ( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) : 1308 0; 1309 } 1310 1311 return compare & 4 ? -1 : 1; 1312 } 1313 1314 // Not directly comparable, sort on existence of method 1315 return a.compareDocumentPosition ? -1 : 1; 1316 } : 1317 function( a, b ) { 1318 var cur, 1319 i = 0, 1320 aup = a.parentNode, 1321 bup = b.parentNode, 1322 ap = [ a ], 1323 bp = [ b ]; 1324 1325 // Exit early if the nodes are identical 1326 if ( a === b ) { 1327 hasDuplicate = true; 1328 return 0; 1329 1330 // Parentless nodes are either documents or disconnected 1331 } else if ( !aup || !bup ) { 1332 return a === doc ? -1 : 1333 b === doc ? 1 : 1334 aup ? -1 : 1335 bup ? 1 : 1336 0; 1337 1338 // If the nodes are siblings, we can do a quick check 1339 } else if ( aup === bup ) { 1340 return siblingCheck( a, b ); 1341 } 1342 1343 // Otherwise we need full lists of their ancestors for comparison 1344 cur = a; 1345 while ( (cur = cur.parentNode) ) { 1346 ap.unshift( cur ); 1347 } 1348 cur = b; 1349 while ( (cur = cur.parentNode) ) { 1350 bp.unshift( cur ); 1351 } 1352 1353 // Walk down the tree looking for a discrepancy 1354 while ( ap[i] === bp[i] ) { 1355 i++; 1356 } 1357 1358 return i ? 1359 // Do a sibling check if the nodes have a common ancestor 1360 siblingCheck( ap[i], bp[i] ) : 1361 1362 // Otherwise nodes in our document sort first 1363 ap[i] === preferredDoc ? -1 : 1364 bp[i] === preferredDoc ? 1 : 1365 0; 1366 }; 1367 1368 return document; 1369 }; 1370 1371 Sizzle.matches = function( expr, elements ) { 1372 return Sizzle( expr, null, null, elements ); 1373 }; 1374 1375 Sizzle.matchesSelector = function( elem, expr ) { 1376 // Set document vars if needed 1377 if ( ( elem.ownerDocument || elem ) !== document ) { 1378 setDocument( elem ); 1379 } 1380 1381 // Make sure that attribute selectors are quoted 1382 expr = expr.replace( rattributeQuotes, "='$1']" ); 1383 1384 // rbuggyQSA always contains :focus, so no need for an existence check 1385 if ( support.matchesSelector && documentIsHTML && (!rbuggyMatches || !rbuggyMatches.test(expr)) && !rbuggyQSA.test(expr) ) { 1386 try { 1387 var ret = matches.call( elem, expr ); 1388 1389 // IE 9's matchesSelector returns false on disconnected nodes 1390 if ( ret || support.disconnectedMatch || 1391 // As well, disconnected nodes are said to be in a document 1392 // fragment in IE 9 1393 elem.document && elem.document.nodeType !== 11 ) { 1394 return ret; 1395 } 1396 } catch(e) {} 1397 } 1398 1399 return Sizzle( expr, document, null, [elem] ).length > 0; 1400 }; 1401 1402 Sizzle.contains = function( context, elem ) { 1403 // Set document vars if needed 1404 if ( ( context.ownerDocument || context ) !== document ) { 1405 setDocument( context ); 1406 } 1407 return contains( context, elem ); 1408 }; 1409 1410 Sizzle.attr = function( elem, name ) { 1411 var val; 1412 1413 // Set document vars if needed 1414 if ( ( elem.ownerDocument || elem ) !== document ) { 1415 setDocument( elem ); 1416 } 1417 1418 if ( documentIsHTML ) { 1419 name = name.toLowerCase(); 1420 } 1421 if ( (val = Expr.attrHandle[ name ]) ) { 1422 return val( elem ); 1423 } 1424 if ( !documentIsHTML || support.attributes ) { 1425 return elem.getAttribute( name ); 1426 } 1427 return ( (val = elem.getAttributeNode( name )) || elem.getAttribute( name ) ) && elem[ name ] === true ? 1428 name : 1429 val && val.specified ? val.value : null; 1430 }; 1431 1432 Sizzle.error = function( msg ) { 1433 throw new Error( "Syntax error, unrecognized expression: " + msg ); 1434 }; 1435 1436 // Document sorting and removing duplicates 1437 Sizzle.uniqueSort = function( results ) { 1438 var elem, 1439 duplicates = [], 1440 j = 0, 1441 i = 0; 1442 1443 // Unless we *know* we can detect duplicates, assume their presence 1444 hasDuplicate = !support.detectDuplicates; 1445 // Compensate for sort limitations 1446 recompare = !support.sortDetached; 1447 sortInput = !support.sortStable && results.slice( 0 ); 1448 results.sort( sortOrder ); 1449 1450 if ( hasDuplicate ) { 1451 while ( (elem = results[i++]) ) { 1452 if ( elem === results[ i ] ) { 1453 j = duplicates.push( i ); 1454 } 1455 } 1456 while ( j-- ) { 1457 results.splice( duplicates[ j ], 1 ); 1458 } 1459 } 1460 1461 return results; 1462 }; 1463 1464 /** 1465 * Checks document order of two siblings 1466 * @param {Element} a 1467 * @param {Element} b 1468 * @returns Returns -1 if a precedes b, 1 if a follows b 1469 */ 1470 function siblingCheck( a, b ) { 1471 var cur = b && a, 1472 diff = cur && ( ~b.sourceIndex || MAX_NEGATIVE ) - ( ~a.sourceIndex || MAX_NEGATIVE ); 1473 1474 // Use IE sourceIndex if available on both nodes 1475 if ( diff ) { 1476 return diff; 1477 } 1478 1479 // Check if b follows a 1480 if ( cur ) { 1481 while ( (cur = cur.nextSibling) ) { 1482 if ( cur === b ) { 1483 return -1; 1484 } 1485 } 1486 } 1487 1488 return a ? 1 : -1; 1489 } 1490 1491 // Returns a function to use in pseudos for input types 1492 function createInputPseudo( type ) { 1493 return function( elem ) { 1494 var name = elem.nodeName.toLowerCase(); 1495 return name === "input" && elem.type === type; 1496 }; 1497 } 1498 1499 // Returns a function to use in pseudos for buttons 1500 function createButtonPseudo( type ) { 1501 return function( elem ) { 1502 var name = elem.nodeName.toLowerCase(); 1503 return (name === "input" || name === "button") && elem.type === type; 1504 }; 1505 } 1506 1507 // Returns a function to use in pseudos for positionals 1508 function createPositionalPseudo( fn ) { 1509 return markFunction(function( argument ) { 1510 argument = +argument; 1511 return markFunction(function( seed, matches ) { 1512 var j, 1513 matchIndexes = fn( [], seed.length, argument ), 1514 i = matchIndexes.length; 1515 1516 // Match elements found at the specified indexes 1517 while ( i-- ) { 1518 if ( seed[ (j = matchIndexes[i]) ] ) { 1519 seed[j] = !(matches[j] = seed[j]); 1520 } 1521 } 1522 }); 1523 }); 1524 } 1525 1526 /** 1527 * Utility function for retrieving the text value of an array of DOM nodes 1528 * @param {Array|Element} elem 1529 */ 1530 getText = Sizzle.getText = function( elem ) { 1531 var node, 1532 ret = "", 1533 i = 0, 1534 nodeType = elem.nodeType; 1535 1536 if ( !nodeType ) { 1537 // If no nodeType, this is expected to be an array 1538 for ( ; (node = elem[i]); i++ ) { 1539 // Do not traverse comment nodes 1540 ret += getText( node ); 1541 } 1542 } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { 1543 // Use textContent for elements 1544 // innerText usage removed for consistency of new lines (see #11153) 1545 if ( typeof elem.textContent === "string" ) { 1546 return elem.textContent; 1547 } else { 1548 // Traverse its children 1549 for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { 1550 ret += getText( elem ); 1551 } 1552 } 1553 } else if ( nodeType === 3 || nodeType === 4 ) { 1554 return elem.nodeValue; 1555 } 1556 // Do not include comment or processing instruction nodes 1557 1558 return ret; 1559 }; 1560 1561 Expr = Sizzle.selectors = { 1562 1563 // Can be adjusted by the user 1564 cacheLength: 50, 1565 1566 createPseudo: markFunction, 1567 1568 match: matchExpr, 1569 1570 find: {}, 1571 1572 relative: { 1573 ">": { dir: "parentNode", first: true }, 1574 " ": { dir: "parentNode" }, 1575 "+": { dir: "previousSibling", first: true }, 1576 "~": { dir: "previousSibling" } 1577 }, 1578 1579 preFilter: { 1580 "ATTR": function( match ) { 1581 match[1] = match[1].replace( runescape, funescape ); 1582 1583 // Move the given value to match[3] whether quoted or unquoted 1584 match[3] = ( match[4] || match[5] || "" ).replace( runescape, funescape ); 1585 1586 if ( match[2] === "~=" ) { 1587 match[3] = " " + match[3] + " "; 1588 } 1589 1590 return match.slice( 0, 4 ); 1591 }, 1592 1593 "CHILD": function( match ) { 1594 /* matches from matchExpr["CHILD"] 1595 1 type (only|nth|...) 1596 2 what (child|of-type) 1597 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...) 1598 4 xn-component of xn+y argument ([+-]?\d*n|) 1599 5 sign of xn-component 1600 6 x of xn-component 1601 7 sign of y-component 1602 8 y of y-component 1603 */ 1604 match[1] = match[1].toLowerCase(); 1605 1606 if ( match[1].slice( 0, 3 ) === "nth" ) { 1607 // nth-* requires argument 1608 if ( !match[3] ) { 1609 Sizzle.error( match[0] ); 1610 } 1611 1612 // numeric x and y parameters for Expr.filter.CHILD 1613 // remember that false/true cast respectively to 0/1 1614 match[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === "even" || match[3] === "odd" ) ); 1615 match[5] = +( ( match[7] + match[8] ) || match[3] === "odd" ); 1616 1617 // other types prohibit arguments 1618 } else if ( match[3] ) { 1619 Sizzle.error( match[0] ); 1620 } 1621 1622 return match; 1623 }, 1624 1625 "PSEUDO": function( match ) { 1626 var excess, 1627 unquoted = !match[5] && match[2]; 1628 1629 if ( matchExpr["CHILD"].test( match[0] ) ) { 1630 return null; 1631 } 1632 1633 // Accept quoted arguments as-is 1634 if ( match[4] ) { 1635 match[2] = match[4]; 1636 1637 // Strip excess characters from unquoted arguments 1638 } else if ( unquoted && rpseudo.test( unquoted ) && 1639 // Get excess from tokenize (recursively) 1640 (excess = tokenize( unquoted, true )) && 1641 // advance to the next closing parenthesis 1642 (excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) { 1643 1644 // excess is a negative index 1645 match[0] = match[0].slice( 0, excess ); 1646 match[2] = unquoted.slice( 0, excess ); 1647 } 1648 1649 // Return only captures needed by the pseudo filter method (type and argument) 1650 return match.slice( 0, 3 ); 1651 } 1652 }, 1653 1654 filter: { 1655 1656 "TAG": function( nodeName ) { 1657 if ( nodeName === "*" ) { 1658 return function() { return true; }; 1659 } 1660 1661 nodeName = nodeName.replace( runescape, funescape ).toLowerCase(); 1662 return function( elem ) { 1663 return elem.nodeName && elem.nodeName.toLowerCase() === nodeName; 1664 }; 1665 }, 1666 1667 "CLASS": function( className ) { 1668 var pattern = classCache[ className + " " ]; 1669 1670 return pattern || 1671 (pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) && 1672 classCache( className, function( elem ) { 1673 return pattern.test( elem.className || (typeof elem.getAttribute !== strundefined && elem.getAttribute("class")) || "" ); 1674 }); 1675 }, 1676 1677 "ATTR": function( name, operator, check ) { 1678 return function( elem ) { 1679 var result = Sizzle.attr( elem, name ); 1680 1681 if ( result == null ) { 1682 return operator === "!="; 1683 } 1684 if ( !operator ) { 1685 return true; 1686 } 1687 1688 result += ""; 1689 1690 return operator === "=" ? result === check : 1691 operator === "!=" ? result !== check : 1692 operator === "^=" ? check && result.indexOf( check ) === 0 : 1693 operator === "*=" ? check && result.indexOf( check ) > -1 : 1694 operator === "$=" ? check && result.slice( -check.length ) === check : 1695 operator === "~=" ? ( " " + result + " " ).indexOf( check ) > -1 : 1696 operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" : 1697 false; 1698 }; 1699 }, 1700 1701 "CHILD": function( type, what, argument, first, last ) { 1702 var simple = type.slice( 0, 3 ) !== "nth", 1703 forward = type.slice( -4 ) !== "last", 1704 ofType = what === "of-type"; 1705 1706 return first === 1 && last === 0 ? 1707 1708 // Shortcut for :nth-*(n) 1709 function( elem ) { 1710 return !!elem.parentNode; 1711 } : 1712 1713 function( elem, context, xml ) { 1714 var cache, outerCache, node, diff, nodeIndex, start, 1715 dir = simple !== forward ? "nextSibling" : "previousSibling", 1716 parent = elem.parentNode, 1717 name = ofType && elem.nodeName.toLowerCase(), 1718 useCache = !xml && !ofType; 1719 1720 if ( parent ) { 1721 1722 // :(first|last|only)-(child|of-type) 1723 if ( simple ) { 1724 while ( dir ) { 1725 node = elem; 1726 while ( (node = node[ dir ]) ) { 1727 if ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) { 1728 return false; 1729 } 1730 } 1731 // Reverse direction for :only-* (if we haven't yet done so) 1732 start = dir = type === "only" && !start && "nextSibling"; 1733 } 1734 return true; 1735 } 1736 1737 start = [ forward ? parent.firstChild : parent.lastChild ]; 1738 1739 // non-xml :nth-child(...) stores cache data on `parent` 1740 if ( forward && useCache ) { 1741 // Seek `elem` from a previously-cached index 1742 outerCache = parent[ expando ] || (parent[ expando ] = {}); 1743 cache = outerCache[ type ] || []; 1744 nodeIndex = cache[0] === dirruns && cache[1]; 1745 diff = cache[0] === dirruns && cache[2]; 1746 node = nodeIndex && parent.childNodes[ nodeIndex ]; 1747 1748 while ( (node = ++nodeIndex && node && node[ dir ] || 1749 1750 // Fallback to seeking `elem` from the start 1751 (diff = nodeIndex = 0) || start.pop()) ) { 1752 1753 // When found, cache indexes on `parent` and break 1754 if ( node.nodeType === 1 && ++diff && node === elem ) { 1755 outerCache[ type ] = [ dirruns, nodeIndex, diff ]; 1756 break; 1757 } 1758 } 1759 1760 // Use previously-cached element index if available 1761 } else if ( useCache && (cache = (elem[ expando ] || (elem[ expando ] = {}))[ type ]) && cache[0] === dirruns ) { 1762 diff = cache[1]; 1763 1764 // xml :nth-child(...) or :nth-last-child(...) or :nth(-last)?-of-type(...) 1765 } else { 1766 // Use the same loop as above to seek `elem` from the start 1767 while ( (node = ++nodeIndex && node && node[ dir ] || 1768 (diff = nodeIndex = 0) || start.pop()) ) { 1769 1770 if ( ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) && ++diff ) { 1771 // Cache the index of each encountered element 1772 if ( useCache ) { 1773 (node[ expando ] || (node[ expando ] = {}))[ type ] = [ dirruns, diff ]; 1774 } 1775 1776 if ( node === elem ) { 1777 break; 1778 } 1779 } 1780 } 1781 } 1782 1783 // Incorporate the offset, then check against cycle size 1784 diff -= last; 1785 return diff === first || ( diff % first === 0 && diff / first >= 0 ); 1786 } 1787 }; 1788 }, 1789 1790 "PSEUDO": function( pseudo, argument ) { 1791 // pseudo-class names are case-insensitive 1792 // http://www.w3.org/TR/selectors/#pseudo-classes 1793 // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters 1794 // Remember that setFilters inherits from pseudos 1795 var args, 1796 fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] || 1797 Sizzle.error( "unsupported pseudo: " + pseudo ); 1798 1799 // The user may use createPseudo to indicate that 1800 // arguments are needed to create the filter function 1801 // just as Sizzle does 1802 if ( fn[ expando ] ) { 1803 return fn( argument ); 1804 } 1805 1806 // But maintain support for old signatures 1807 if ( fn.length > 1 ) { 1808 args = [ pseudo, pseudo, "", argument ]; 1809 return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ? 1810 markFunction(function( seed, matches ) { 1811 var idx, 1812 matched = fn( seed, argument ), 1813 i = matched.length; 1814 while ( i-- ) { 1815 idx = indexOf.call( seed, matched[i] ); 1816 seed[ idx ] = !( matches[ idx ] = matched[i] ); 1817 } 1818 }) : 1819 function( elem ) { 1820 return fn( elem, 0, args ); 1821 }; 1822 } 1823 1824 return fn; 1825 } 1826 }, 1827 1828 pseudos: { 1829 // Potentially complex pseudos 1830 "not": markFunction(function( selector ) { 1831 // Trim the selector passed to compile 1832 // to avoid treating leading and trailing 1833 // spaces as combinators 1834 var input = [], 1835 results = [], 1836 matcher = compile( selector.replace( rtrim, "$1" ) ); 1837 1838 return matcher[ expando ] ? 1839 markFunction(function( seed, matches, context, xml ) { 1840 var elem, 1841 unmatched = matcher( seed, null, xml, [] ), 1842 i = seed.length; 1843 1844 // Match elements unmatched by `matcher` 1845 while ( i-- ) { 1846 if ( (elem = unmatched[i]) ) { 1847 seed[i] = !(matches[i] = elem); 1848 } 1849 } 1850 }) : 1851 function( elem, context, xml ) { 1852 input[0] = elem; 1853 matcher( input, null, xml, results ); 1854 return !results.pop(); 1855 }; 1856 }), 1857 1858 "has": markFunction(function( selector ) { 1859 return function( elem ) { 1860 return Sizzle( selector, elem ).length > 0; 1861 }; 1862 }), 1863 1864 "contains": markFunction(function( text ) { 1865 return function( elem ) { 1866 return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1; 1867 }; 1868 }), 1869 1870 // "Whether an element is represented by a :lang() selector 1871 // is based solely on the element's language value 1872 // being equal to the identifier C, 1873 // or beginning with the identifier C immediately followed by "-". 1874 // The matching of C against the element's language value is performed case-insensitively. 1875 // The identifier C does not have to be a valid language name." 1876 // http://www.w3.org/TR/selectors/#lang-pseudo 1877 "lang": markFunction( function( lang ) { 1878 // lang value must be a valid identifier 1879 if ( !ridentifier.test(lang || "") ) { 1880 Sizzle.error( "unsupported lang: " + lang ); 1881 } 1882 lang = lang.replace( runescape, funescape ).toLowerCase(); 1883 return function( elem ) { 1884 var elemLang; 1885 do { 1886 if ( (elemLang = documentIsHTML ? 1887 elem.lang : 1888 elem.getAttribute("xml:lang") || elem.getAttribute("lang")) ) { 1889 1890 elemLang = elemLang.toLowerCase(); 1891 return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0; 1892 } 1893 } while ( (elem = elem.parentNode) && elem.nodeType === 1 ); 1894 return false; 1895 }; 1896 }), 1897 1898 // Miscellaneous 1899 "target": function( elem ) { 1900 var hash = window.location && window.location.hash; 1901 return hash && hash.slice( 1 ) === elem.id; 1902 }, 1903 1904 "root": function( elem ) { 1905 return elem === docElem; 1906 }, 1907 1908 "focus": function( elem ) { 1909 return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex); 1910 }, 1911 1912 // Boolean properties 1913 "enabled": function( elem ) { 1914 return elem.disabled === false; 1915 }, 1916 1917 "disabled": function( elem ) { 1918 return elem.disabled === true; 1919 }, 1920 1921 "checked": function( elem ) { 1922 // In CSS3, :checked should return both checked and selected elements 1923 // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked 1924 var nodeName = elem.nodeName.toLowerCase(); 1925 return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected); 1926 }, 1927 1928 "selected": function( elem ) { 1929 // Accessing this property makes selected-by-default 1930 // options in Safari work properly 1931 if ( elem.parentNode ) { 1932 elem.parentNode.selectedIndex; 1933 } 1934 1935 return elem.selected === true; 1936 }, 1937 1938 // Contents 1939 "empty": function( elem ) { 1940 // http://www.w3.org/TR/selectors/#empty-pseudo 1941 // :empty is only affected by element nodes and content nodes(including text(3), cdata(4)), 1942 // not comment, processing instructions, or others 1943 // Thanks to Diego Perini for the nodeName shortcut 1944 // Greater than "@" means alpha characters (specifically not starting with "#" or "?") 1945 for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { 1946 if ( elem.nodeName > "@" || elem.nodeType === 3 || elem.nodeType === 4 ) { 1947 return false; 1948 } 1949 } 1950 return true; 1951 }, 1952 1953 "parent": function( elem ) { 1954 return !Expr.pseudos["empty"]( elem ); 1955 }, 1956 1957 // Element/input types 1958 "header": function( elem ) { 1959 return rheader.test( elem.nodeName ); 1960 }, 1961 1962 "input": function( elem ) { 1963 return rinputs.test( elem.nodeName ); 1964 }, 1965 1966 "button": function( elem ) { 1967 var name = elem.nodeName.toLowerCase(); 1968 return name === "input" && elem.type === "button" || name === "button"; 1969 }, 1970 1971 "text": function( elem ) { 1972 var attr; 1973 // IE6 and 7 will map elem.type to 'text' for new HTML5 types (search, etc) 1974 // use getAttribute instead to test this case 1975 return elem.nodeName.toLowerCase() === "input" && 1976 elem.type === "text" && 1977 ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === elem.type ); 1978 }, 1979 1980 // Position-in-collection 1981 "first": createPositionalPseudo(function() { 1982 return [ 0 ]; 1983 }), 1984 1985 "last": createPositionalPseudo(function( matchIndexes, length ) { 1986 return [ length - 1 ]; 1987 }), 1988 1989 "eq": createPositionalPseudo(function( matchIndexes, length, argument ) { 1990 return [ argument < 0 ? argument + length : argument ]; 1991 }), 1992 1993 "even": createPositionalPseudo(function( matchIndexes, length ) { 1994 var i = 0; 1995 for ( ; i < length; i += 2 ) { 1996 matchIndexes.push( i ); 1997 } 1998 return matchIndexes; 1999 }), 2000 2001 "odd": createPositionalPseudo(function( matchIndexes, length ) { 2002 var i = 1; 2003 for ( ; i < length; i += 2 ) { 2004 matchIndexes.push( i ); 2005 } 2006 return matchIndexes; 2007 }), 2008 2009 "lt": createPositionalPseudo(function( matchIndexes, length, argument ) { 2010 var i = argument < 0 ? argument + length : argument; 2011 for ( ; --i >= 0; ) { 2012 matchIndexes.push( i ); 2013 } 2014 return matchIndexes; 2015 }), 2016 2017 "gt": createPositionalPseudo(function( matchIndexes, length, argument ) { 2018 var i = argument < 0 ? argument + length : argument; 2019 for ( ; ++i < length; ) { 2020 matchIndexes.push( i ); 2021 } 2022 return matchIndexes; 2023 }) 2024 } 2025 }; 2026 2027 // Add button/input type pseudos 2028 for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) { 2029 Expr.pseudos[ i ] = createInputPseudo( i ); 2030 } 2031 for ( i in { submit: true, reset: true } ) { 2032 Expr.pseudos[ i ] = createButtonPseudo( i ); 2033 } 2034 2035 function tokenize( selector, parseOnly ) { 2036 var matched, match, tokens, type, 2037 soFar, groups, preFilters, 2038 cached = tokenCache[ selector + " " ]; 2039 2040 if ( cached ) { 2041 return parseOnly ? 0 : cached.slice( 0 ); 2042 } 2043 2044 soFar = selector; 2045 groups = []; 2046 preFilters = Expr.preFilter; 2047 2048 while ( soFar ) { 2049 2050 // Comma and first run 2051 if ( !matched || (match = rcomma.exec( soFar )) ) { 2052 if ( match ) { 2053 // Don't consume trailing commas as valid 2054 soFar = soFar.slice( match[0].length ) || soFar; 2055 } 2056 groups.push( tokens = [] ); 2057 } 2058 2059 matched = false; 2060 2061 // Combinators 2062 if ( (match = rcombinators.exec( soFar )) ) { 2063 matched = match.shift(); 2064 tokens.push( { 2065 value: matched, 2066 // Cast descendant combinators to space 2067 type: match[0].replace( rtrim, " " ) 2068 } ); 2069 soFar = soFar.slice( matched.length ); 2070 } 2071 2072 // Filters 2073 for ( type in Expr.filter ) { 2074 if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] || 2075 (match = preFilters[ type ]( match ))) ) { 2076 matched = match.shift(); 2077 tokens.push( { 2078 value: matched, 2079 type: type, 2080 matches: match 2081 } ); 2082 soFar = soFar.slice( matched.length ); 2083 } 2084 } 2085 2086 if ( !matched ) { 2087 break; 2088 } 2089 } 2090 2091 // Return the length of the invalid excess 2092 // if we're just parsing 2093 // Otherwise, throw an error or return tokens 2094 return parseOnly ? 2095 soFar.length : 2096 soFar ? 2097 Sizzle.error( selector ) : 2098 // Cache the tokens 2099 tokenCache( selector, groups ).slice( 0 ); 2100 } 2101 2102 function toSelector( tokens ) { 2103 var i = 0, 2104 len = tokens.length, 2105 selector = ""; 2106 for ( ; i < len; i++ ) { 2107 selector += tokens[i].value; 2108 } 2109 return selector; 2110 } 2111 2112 function addCombinator( matcher, combinator, base ) { 2113 var dir = combinator.dir, 2114 checkNonElements = base && dir === "parentNode", 2115 doneName = done++; 2116 2117 return combinator.first ? 2118 // Check against closest ancestor/preceding element 2119 function( elem, context, xml ) { 2120 while ( (elem = elem[ dir ]) ) { 2121 if ( elem.nodeType === 1 || checkNonElements ) { 2122 return matcher( elem, context, xml ); 2123 } 2124 } 2125 } : 2126 2127 // Check against all ancestor/preceding elements 2128 function( elem, context, xml ) { 2129 var data, cache, outerCache, 2130 dirkey = dirruns + " " + doneName; 2131 2132 // We can't set arbitrary data on XML nodes, so they don't benefit from dir caching 2133 if ( xml ) { 2134 while ( (elem = elem[ dir ]) ) { 2135 if ( elem.nodeType === 1 || checkNonElements ) { 2136 if ( matcher( elem, context, xml ) ) { 2137 return true; 2138 } 2139 } 2140 } 2141 } else { 2142 while ( (elem = elem[ dir ]) ) { 2143 if ( elem.nodeType === 1 || checkNonElements ) { 2144 outerCache = elem[ expando ] || (elem[ expando ] = {}); 2145 if ( (cache = outerCache[ dir ]) && cache[0] === dirkey ) { 2146 if ( (data = cache[1]) === true || data === cachedruns ) { 2147 return data === true; 2148 } 2149 } else { 2150 cache = outerCache[ dir ] = [ dirkey ]; 2151 cache[1] = matcher( elem, context, xml ) || cachedruns; 2152 if ( cache[1] === true ) { 2153 return true; 2154 } 2155 } 2156 } 2157 } 2158 } 2159 }; 2160 } 2161 2162 function elementMatcher( matchers ) { 2163 return matchers.length > 1 ? 2164 function( elem, context, xml ) { 2165 var i = matchers.length; 2166 while ( i-- ) { 2167 if ( !matchers[i]( elem, context, xml ) ) { 2168 return false; 2169 } 2170 } 2171 return true; 2172 } : 2173 matchers[0]; 2174 } 2175 2176 function condense( unmatched, map, filter, context, xml ) { 2177 var elem, 2178 newUnmatched = [], 2179 i = 0, 2180 len = unmatched.length, 2181 mapped = map != null; 2182 2183 for ( ; i < len; i++ ) { 2184 if ( (elem = unmatched[i]) ) { 2185 if ( !filter || filter( elem, context, xml ) ) { 2186 newUnmatched.push( elem ); 2187 if ( mapped ) { 2188 map.push( i ); 2189 } 2190 } 2191 } 2192 } 2193 2194 return newUnmatched; 2195 } 2196 2197 function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) { 2198 if ( postFilter && !postFilter[ expando ] ) { 2199 postFilter = setMatcher( postFilter ); 2200 } 2201 if ( postFinder && !postFinder[ expando ] ) { 2202 postFinder = setMatcher( postFinder, postSelector ); 2203 } 2204 return markFunction(function( seed, results, context, xml ) { 2205 var temp, i, elem, 2206 preMap = [], 2207 postMap = [], 2208 preexisting = results.length, 2209 2210 // Get initial elements from seed or context 2211 elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ), 2212 2213 // Prefilter to get matcher input, preserving a map for seed-results synchronization 2214 matcherIn = preFilter && ( seed || !selector ) ? 2215 condense( elems, preMap, preFilter, context, xml ) : 2216 elems, 2217 2218 matcherOut = matcher ? 2219 // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results, 2220 postFinder || ( seed ? preFilter : preexisting || postFilter ) ? 2221 2222 // ...intermediate processing is necessary 2223 [] : 2224 2225 // ...otherwise use results directly 2226 results : 2227 matcherIn; 2228 2229 // Find primary matches 2230 if ( matcher ) { 2231 matcher( matcherIn, matcherOut, context, xml ); 2232 } 2233 2234 // Apply postFilter 2235 if ( postFilter ) { 2236 temp = condense( matcherOut, postMap ); 2237 postFilter( temp, [], context, xml ); 2238 2239 // Un-match failing elements by moving them back to matcherIn 2240 i = temp.length; 2241 while ( i-- ) { 2242 if ( (elem = temp[i]) ) { 2243 matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem); 2244 } 2245 } 2246 } 2247 2248 if ( seed ) { 2249 if ( postFinder || preFilter ) { 2250 if ( postFinder ) { 2251 // Get the final matcherOut by condensing this intermediate into postFinder contexts 2252 temp = []; 2253 i = matcherOut.length; 2254 while ( i-- ) { 2255 if ( (elem = matcherOut[i]) ) { 2256 // Restore matcherIn since elem is not yet a final match 2257 temp.push( (matcherIn[i] = elem) ); 2258 } 2259 } 2260 postFinder( null, (matcherOut = []), temp, xml ); 2261 } 2262 2263 // Move matched elements from seed to results to keep them synchronized 2264 i = matcherOut.length; 2265 while ( i-- ) { 2266 if ( (elem = matcherOut[i]) && 2267 (temp = postFinder ? indexOf.call( seed, elem ) : preMap[i]) > -1 ) { 2268 2269 seed[temp] = !(results[temp] = elem); 2270 } 2271 } 2272 } 2273 2274 // Add elements to results, through postFinder if defined 2275 } else { 2276 matcherOut = condense( 2277 matcherOut === results ? 2278 matcherOut.splice( preexisting, matcherOut.length ) : 2279 matcherOut 2280 ); 2281 if ( postFinder ) { 2282 postFinder( null, results, matcherOut, xml ); 2283 } else { 2284 push.apply( results, matcherOut ); 2285 } 2286 } 2287 }); 2288 } 2289 2290 function matcherFromTokens( tokens ) { 2291 var checkContext, matcher, j, 2292 len = tokens.length, 2293 leadingRelative = Expr.relative[ tokens[0].type ], 2294 implicitRelative = leadingRelative || Expr.relative[" "], 2295 i = leadingRelative ? 1 : 0, 2296 2297 // The foundational matcher ensures that elements are reachable from top-level context(s) 2298 matchContext = addCombinator( function( elem ) { 2299 return elem === checkContext; 2300 }, implicitRelative, true ), 2301 matchAnyContext = addCombinator( function( elem ) { 2302 return indexOf.call( checkContext, elem ) > -1; 2303 }, implicitRelative, true ), 2304 matchers = [ function( elem, context, xml ) { 2305 return ( !leadingRelative && ( xml || context !== outermostContext ) ) || ( 2306 (checkContext = context).nodeType ? 2307 matchContext( elem, context, xml ) : 2308 matchAnyContext( elem, context, xml ) ); 2309 } ]; 2310 2311 for ( ; i < len; i++ ) { 2312 if ( (matcher = Expr.relative[ tokens[i].type ]) ) { 2313 matchers = [ addCombinator(elementMatcher( matchers ), matcher) ]; 2314 } else { 2315 matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches ); 2316 2317 // Return special upon seeing a positional matcher 2318 if ( matcher[ expando ] ) { 2319 // Find the next relative operator (if any) for proper handling 2320 j = ++i; 2321 for ( ; j < len; j++ ) { 2322 if ( Expr.relative[ tokens[j].type ] ) { 2323 break; 2324 } 2325 } 2326 return setMatcher( 2327 i > 1 && elementMatcher( matchers ), 2328 i > 1 && toSelector( tokens.slice( 0, i - 1 ) ).replace( rtrim, "$1" ), 2329 matcher, 2330 i < j && matcherFromTokens( tokens.slice( i, j ) ), 2331 j < len && matcherFromTokens( (tokens = tokens.slice( j )) ), 2332 j < len && toSelector( tokens ) 2333 ); 2334 } 2335 matchers.push( matcher ); 2336 } 2337 } 2338 2339 return elementMatcher( matchers ); 2340 } 2341 2342 function matcherFromGroupMatchers( elementMatchers, setMatchers ) { 2343 // A counter to specify which element is currently being matched 2344 var matcherCachedRuns = 0, 2345 bySet = setMatchers.length > 0, 2346 byElement = elementMatchers.length > 0, 2347 superMatcher = function( seed, context, xml, results, expandContext ) { 2348 var elem, j, matcher, 2349 setMatched = [], 2350 matchedCount = 0, 2351 i = "0", 2352 unmatched = seed && [], 2353 outermost = expandContext != null, 2354 contextBackup = outermostContext, 2355 // We must always have either seed elements or context 2356 elems = seed || byElement && Expr.find["TAG"]( "*", expandContext && context.parentNode || context ), 2357 // Use integer dirruns iff this is the outermost matcher 2358 dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1); 2359 2360 if ( outermost ) { 2361 outermostContext = context !== document && context; 2362 cachedruns = matcherCachedRuns; 2363 } 2364 2365 // Add elements passing elementMatchers directly to results 2366 // Keep `i` a string if there are no elements so `matchedCount` will be "00" below 2367 for ( ; (elem = elems[i]) != null; i++ ) { 2368 if ( byElement && elem ) { 2369 j = 0; 2370 while ( (matcher = elementMatchers[j++]) ) { 2371 if ( matcher( elem, context, xml ) ) { 2372 results.push( elem ); 2373 break; 2374 } 2375 } 2376 if ( outermost ) { 2377 dirruns = dirrunsUnique; 2378 cachedruns = ++matcherCachedRuns; 2379 } 2380 } 2381 2382 // Track unmatched elements for set filters 2383 if ( bySet ) { 2384 // They will have gone through all possible matchers 2385 if ( (elem = !matcher && elem) ) { 2386 matchedCount--; 2387 } 2388 2389 // Lengthen the array for every element, matched or not 2390 if ( seed ) { 2391 unmatched.push( elem ); 2392 } 2393 } 2394 } 2395 2396 // Apply set filters to unmatched elements 2397 matchedCount += i; 2398 if ( bySet && i !== matchedCount ) { 2399 j = 0; 2400 while ( (matcher = setMatchers[j++]) ) { 2401 matcher( unmatched, setMatched, context, xml ); 2402 } 2403 2404 if ( seed ) { 2405 // Reintegrate element matches to eliminate the need for sorting 2406 if ( matchedCount > 0 ) { 2407 while ( i-- ) { 2408 if ( !(unmatched[i] || setMatched[i]) ) { 2409 setMatched[i] = pop.call( results ); 2410 } 2411 } 2412 } 2413 2414 // Discard index placeholder values to get only actual matches 2415 setMatched = condense( setMatched ); 2416 } 2417 2418 // Add matches to results 2419 push.apply( results, setMatched ); 2420 2421 // Seedless set matches succeeding multiple successful matchers stipulate sorting 2422 if ( outermost && !seed && setMatched.length > 0 && 2423 ( matchedCount + setMatchers.length ) > 1 ) { 2424 2425 Sizzle.uniqueSort( results ); 2426 } 2427 } 2428 2429 // Override manipulation of globals by nested matchers 2430 if ( outermost ) { 2431 dirruns = dirrunsUnique; 2432 outermostContext = contextBackup; 2433 } 2434 2435 return unmatched; 2436 }; 2437 2438 return bySet ? 2439 markFunction( superMatcher ) : 2440 superMatcher; 2441 } 2442 2443 compile = Sizzle.compile = function( selector, group /* Internal Use Only */ ) { 2444 var i, 2445 setMatchers = [], 2446 elementMatchers = [], 2447 cached = compilerCache[ selector + " " ]; 2448 2449 if ( !cached ) { 2450 // Generate a function of recursive functions that can be used to check each element 2451 if ( !group ) { 2452 group = tokenize( selector ); 2453 } 2454 i = group.length; 2455 while ( i-- ) { 2456 cached = matcherFromTokens( group[i] ); 2457 if ( cached[ expando ] ) { 2458 setMatchers.push( cached ); 2459 } else { 2460 elementMatchers.push( cached ); 2461 } 2462 } 2463 2464 // Cache the compiled function 2465 cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) ); 2466 } 2467 return cached; 2468 }; 2469 2470 function multipleContexts( selector, contexts, results ) { 2471 var i = 0, 2472 len = contexts.length; 2473 for ( ; i < len; i++ ) { 2474 Sizzle( selector, contexts[i], results ); 2475 } 2476 return results; 2477 } 2478 2479 function select( selector, context, results, seed ) { 2480 var i, tokens, token, type, find, 2481 match = tokenize( selector ); 2482 2483 if ( !seed ) { 2484 // Try to minimize operations if there is only one group 2485 if ( match.length === 1 ) { 2486 2487 // Take a shortcut and set the context if the root selector is an ID 2488 tokens = match[0] = match[0].slice( 0 ); 2489 if ( tokens.length > 2 && (token = tokens[0]).type === "ID" && 2490 context.nodeType === 9 && documentIsHTML && 2491 Expr.relative[ tokens[1].type ] ) { 2492 2493 context = ( Expr.find["ID"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0]; 2494 if ( !context ) { 2495 return results; 2496 } 2497 2498 selector = selector.slice( tokens.shift().value.length ); 2499 } 2500 2501 // Fetch a seed set for right-to-left matching 2502 i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length; 2503 while ( i-- ) { 2504 token = tokens[i]; 2505 2506 // Abort if we hit a combinator 2507 if ( Expr.relative[ (type = token.type) ] ) { 2508 break; 2509 } 2510 if ( (find = Expr.find[ type ]) ) { 2511 // Search, expanding context for leading sibling combinators 2512 if ( (seed = find( 2513 token.matches[0].replace( runescape, funescape ), 2514 rsibling.test( tokens[0].type ) && context.parentNode || context 2515 )) ) { 2516 2517 // If seed is empty or no tokens remain, we can return early 2518 tokens.splice( i, 1 ); 2519 selector = seed.length && toSelector( tokens ); 2520 if ( !selector ) { 2521 push.apply( results, seed ); 2522 return results; 2523 } 2524 2525 break; 2526 } 2527 } 2528 } 2529 } 2530 } 2531 2532 // Compile and execute a filtering function 2533 // Provide `match` to avoid retokenization if we modified the selector above 2534 compile( selector, match )( 2535 seed, 2536 context, 2537 !documentIsHTML, 2538 results, 2539 rsibling.test( selector ) 2540 ); 2541 return results; 2542 } 2543 2544 // Deprecated 2545 Expr.pseudos["nth"] = Expr.pseudos["eq"]; 2546 2547 // Easy API for creating new setFilters 2548 function setFilters() {} 2549 setFilters.prototype = Expr.filters = Expr.pseudos; 2550 Expr.setFilters = new setFilters(); 2551 2552 // Check sort stability 2553 support.sortStable = expando.split("").sort( sortOrder ).join("") === expando; 2554 2555 // Initialize with the default document 2556 setDocument(); 2557 2558 // Always assume the presence of duplicates if sort doesn't 2559 // pass them to our comparison function (as in Google Chrome). 2560 [0, 0].sort( sortOrder ); 2561 support.detectDuplicates = hasDuplicate; 2562 2563 /* 2564 // EXPOSE 2565 if ( typeof define === "function" && define.amd ) { 2566 define(function() { return Sizzle; }); 2567 } else { 2568 window.Sizzle = Sizzle; 2569 } 2570 */ 2571 2572 // EXPOSE 2573 return Sizzle; 2574 }); 2575 2576 // Included from: js/tinymce/classes/dom/DomQuery.js 2577 2578 /** 2579 * DomQuery.js 2580 * 2581 * Copyright, Moxiecode Systems AB 2582 * Released under LGPL License. 2583 * 2584 * License: http://www.tinymce.com/license 2585 * Contributing: http://www.tinymce.com/contributing 2586 * 2587 * Some of this logic is based on jQuery code that is released under 2588 * MIT license that grants us to sublicense it under LGPL. 2589 * 2590 * @ignore-file 2591 */ 2592 2593 /** 2594 * @class tinymce.dom.DomQuery 2595 */ 2596 define("tinymce/dom/DomQuery", [ 2597 "tinymce/dom/EventUtils", 2598 "tinymce/dom/Sizzle" 2599 ], function(EventUtils, Sizzle) { 2600 var doc = document, push = Array.prototype.push, slice = Array.prototype.slice; 2601 var rquickExpr = /^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/; 2602 var Event = EventUtils.Event; 2603 2604 function isDefined(obj) { 2605 return typeof obj !== "undefined"; 2606 } 2607 2608 function isString(obj) { 2609 return typeof obj === "string"; 2610 } 2611 2612 function createFragment(html) { 2613 var frag, node, container; 2614 2615 container = doc.createElement("div"); 2616 frag = doc.createDocumentFragment(); 2617 container.innerHTML = html; 2618 2619 while ((node = container.firstChild)) { 2620 frag.appendChild(node); 2621 } 2622 2623 return frag; 2624 } 2625 2626 function domManipulate(targetNodes, sourceItem, callback) { 2627 var i; 2628 2629 if (typeof sourceItem === "string") { 2630 sourceItem = createFragment(sourceItem); 2631 } else if (sourceItem.length) { 2632 for (i = 0; i < sourceItem.length; i++) { 2633 domManipulate(targetNodes, sourceItem[i], callback); 2634 } 2635 2636 return targetNodes; 2637 } 2638 2639 i = targetNodes.length; 2640 while (i--) { 2641 callback.call(targetNodes[i], sourceItem.parentNode ? sourceItem : sourceItem); 2642 } 2643 2644 return targetNodes; 2645 } 2646 2647 function hasClass(node, className) { 2648 return node && className && (' ' + node.className + ' ').indexOf(' ' + className + ' ') !== -1; 2649 } 2650 2651 /** 2652 * Makes a map object out of a string that gets separated by a delimiter. 2653 * 2654 * @method makeMap 2655 * @param {String} items Item string to split. 2656 * @param {Object} map Optional object to add items to. 2657 * @return {Object} name/value object with items as keys. 2658 */ 2659 function makeMap(items, map) { 2660 var i; 2661 2662 items = items || []; 2663 2664 if (typeof(items) == "string") { 2665 items = items.split(' '); 2666 } 2667 2668 map = map || {}; 2669 2670 i = items.length; 2671 while (i--) { 2672 map[items[i]] = {}; 2673 } 2674 2675 return map; 2676 } 2677 2678 var numericCssMap = makeMap('fillOpacity fontWeight lineHeight opacity orphans widows zIndex zoom'); 2679 2680 function DomQuery(selector, context) { 2681 /*eslint new-cap:0 */ 2682 return new DomQuery.fn.init(selector, context); 2683 } 2684 2685 /** 2686 * Extends the specified object with another object. 2687 * 2688 * @method extend 2689 * @param {Object} target Object to extend. 2690 * @param {Object..} obj Multiple objects to extend with. 2691 * @return {Object} Same as target, the extended object. 2692 */ 2693 function extend(target) { 2694 var args = arguments, arg, i, key; 2695 2696 for (i = 1; i < args.length; i++) { 2697 arg = args[i]; 2698 2699 for (key in arg) { 2700 target[key] = arg[key]; 2701 } 2702 } 2703 2704 return target; 2705 } 2706 2707 /** 2708 * Converts the specified object into a real JavaScript array. 2709 * 2710 * @method toArray 2711 * @param {Object} obj Object to convert into array. 2712 * @return {Array} Array object based in input. 2713 */ 2714 function toArray(obj) { 2715 var array = [], i, l; 2716 2717 for (i = 0, l = obj.length; i < l; i++) { 2718 array[i] = obj[i]; 2719 } 2720 2721 return array; 2722 } 2723 2724 /** 2725 * Returns the index of the specified item inside the array. 2726 * 2727 * @method inArray 2728 * @param {Object} item Item to look for. 2729 * @param {Array} array Array to look for item in. 2730 * @return {Number} Index of the item or -1. 2731 */ 2732 function inArray(item, array) { 2733 var i; 2734 2735 if (array.indexOf) { 2736 return array.indexOf(item); 2737 } 2738 2739 i = array.length; 2740 while (i--) { 2741 if (array[i] === item) { 2742 return i; 2743 } 2744 } 2745 2746 return -1; 2747 } 2748 2749 /** 2750 * Returns true/false if the specified object is an array. 2751 * 2752 * @method isArray 2753 * @param {Object} obj Object to check if it's an array. 2754 * @return {Boolean} true/false if the input object is array or not. 2755 */ 2756 var isArray = Array.isArray || function(obj) { 2757 return Object.prototype.toString.call(obj) === "[object Array]"; 2758 }; 2759 2760 var whiteSpaceRegExp = /^\s*|\s*$/g; 2761 2762 function trim(str) { 2763 return (str === null || str === undefined) ? '' : ("" + str).replace(whiteSpaceRegExp, ''); 2764 } 2765 2766 /** 2767 * Executes the callback function for each item in array/object. If you return false in the 2768 * callback it will break the loop. 2769 * 2770 * @method each 2771 * @param {Object} obj Object to iterate. 2772 * @param {function} callback Callback function to execute for each item. 2773 */ 2774 function each(obj, callback) { 2775 var length, key, i, undef, value; 2776 2777 if (obj) { 2778 length = obj.length; 2779 2780 if (length === undef) { 2781 // Loop object items 2782 for (key in obj) { 2783 if (obj.hasOwnProperty(key)) { 2784 value = obj[key]; 2785 if (callback.call(value, value, key) === false) { 2786 break; 2787 } 2788 } 2789 } 2790 } else { 2791 // Loop array items 2792 for (i = 0; i < length; i++) { 2793 value = obj[i]; 2794 if (callback.call(value, value, key) === false) { 2795 break; 2796 } 2797 } 2798 } 2799 } 2800 2801 return obj; 2802 } 2803 2804 DomQuery.fn = DomQuery.prototype = { 2805 constructor: DomQuery, 2806 selector: "", 2807 length: 0, 2808 2809 init: function(selector, context) { 2810 var self = this, match, node; 2811 2812 if (!selector) { 2813 return self; 2814 } 2815 2816 if (selector.nodeType) { 2817 self.context = self[0] = selector; 2818 self.length = 1; 2819 2820 return self; 2821 } 2822 2823 if (isString(selector)) { 2824 if (selector.charAt(0) === "<" && selector.charAt(selector.length - 1) === ">" && selector.length >= 3) { 2825 match = [null, selector, null]; 2826 } else { 2827 match = rquickExpr.exec(selector); 2828 } 2829 2830 if (match) { 2831 if (match[1]) { 2832 node = createFragment(selector).firstChild; 2833 while (node) { 2834 this.add(node); 2835 node = node.nextSibling; 2836 } 2837 } else { 2838 node = doc.getElementById(match[2]); 2839 2840 if (node.id !== match[2]) { 2841 return self.find(selector); 2842 } 2843 2844 self.length = 1; 2845 self[0] = node; 2846 } 2847 } else { 2848 return DomQuery(context || document).find(selector); 2849 } 2850 } else { 2851 this.add(selector); 2852 } 2853 2854 return self; 2855 }, 2856 2857 toArray: function() { 2858 return toArray(this); 2859 }, 2860 2861 add: function(items) { 2862 var self = this; 2863 2864 // Force single item into array 2865 if (!isArray(items)) { 2866 if (items instanceof DomQuery) { 2867 self.add(items.toArray()); 2868 } else { 2869 push.call(self, items); 2870 } 2871 } else { 2872 push.apply(self, items); 2873 } 2874 2875 return self; 2876 }, 2877 2878 attr: function(name, value) { 2879 var self = this; 2880 2881 if (typeof name === "object") { 2882 each(name, function(value, name) { 2883 self.attr(name, value); 2884 }); 2885 } else if (isDefined(value)) { 2886 this.each(function() { 2887 if (this.nodeType === 1) { 2888 this.setAttribute(name, value); 2889 } 2890 }); 2891 } else { 2892 return self[0] && self[0].nodeType === 1 ? self[0].getAttribute(name) : undefined; 2893 } 2894 2895 return self; 2896 }, 2897 2898 css: function(name, value) { 2899 var self = this; 2900 2901 if (typeof name === "object") { 2902 each(name, function(value, name) { 2903 self.css(name, value); 2904 }); 2905 } else { 2906 // Camelcase it, if needed 2907 name = name.replace(/-(\D)/g, function(a, b) { 2908 return b.toUpperCase(); 2909 }); 2910 2911 if (isDefined(value)) { 2912 // Default px suffix on these 2913 if (typeof(value) === 'number' && !numericCssMap[name]) { 2914 value += 'px'; 2915 } 2916 2917 self.each(function() { 2918 var style = this.style; 2919 2920 // IE specific opacity 2921 if (name === "opacity" && this.runtimeStyle && typeof(this.runtimeStyle.opacity) === "undefined") { 2922 style.filter = value === '' ? '' : "alpha(opacity=" + (value * 100) + ")"; 2923 } 2924 2925 try { 2926 style[name] = value; 2927 } catch (ex) { 2928 // Ignore 2929 } 2930 }); 2931 } else { 2932 return self[0] ? self[0].style[name] : undefined; 2933 } 2934 } 2935 2936 return self; 2937 }, 2938 2939 remove: function() { 2940 var self = this, node, i = this.length; 2941 2942 while (i--) { 2943 node = self[i]; 2944 Event.clean(node); 2945 2946 if (node.parentNode) { 2947 node.parentNode.removeChild(node); 2948 } 2949 } 2950 2951 return this; 2952 }, 2953 2954 empty: function() { 2955 var self = this, node, i = this.length; 2956 2957 while (i--) { 2958 node = self[i]; 2959 while (node.firstChild) { 2960 node.removeChild(node.firstChild); 2961 } 2962 } 2963 2964 return this; 2965 }, 2966 2967 html: function(value) { 2968 var self = this, i; 2969 2970 if (isDefined(value)) { 2971 i = self.length; 2972 while (i--) { 2973 self[i].innerHTML = value; 2974 } 2975 2976 return self; 2977 } 2978 2979 return self[0] ? self[0].innerHTML : ''; 2980 }, 2981 2982 text: function(value) { 2983 var self = this, i; 2984 2985 if (isDefined(value)) { 2986 i = self.length; 2987 while (i--) { 2988 self[i].innerText = self[0].textContent = value; 2989 } 2990 2991 return self; 2992 } 2993 2994 return self[0] ? self[0].innerText || self[0].textContent : ''; 2995 }, 2996 2997 append: function() { 2998 return domManipulate(this, arguments, function(node) { 2999 if (this.nodeType === 1) { 3000 this.appendChild(node); 3001 } 3002 }); 3003 }, 3004 3005 prepend: function() { 3006 return domManipulate(this, arguments, function(node) { 3007 if (this.nodeType === 1) { 3008 this.insertBefore(node, this.firstChild); 3009 } 3010 }); 3011 }, 3012 3013 before: function() { 3014 var self = this; 3015 3016 if (self[0] && self[0].parentNode) { 3017 return domManipulate(self, arguments, function(node) { 3018 this.parentNode.insertBefore(node, this.nextSibling); 3019 }); 3020 } 3021 3022 return self; 3023 }, 3024 3025 after: function() { 3026 var self = this; 3027 3028 if (self[0] && self[0].parentNode) { 3029 return domManipulate(self, arguments, function(node) { 3030 this.parentNode.insertBefore(node, this); 3031 }); 3032 } 3033 3034 return self; 3035 }, 3036 3037 appendTo: function(val) { 3038 DomQuery(val).append(this); 3039 3040 return this; 3041 }, 3042 3043 addClass: function(className) { 3044 return this.toggleClass(className, true); 3045 }, 3046 3047 removeClass: function(className) { 3048 return this.toggleClass(className, false); 3049 }, 3050 3051 toggleClass: function(className, state) { 3052 var self = this; 3053 3054 if (className.indexOf(' ') !== -1) { 3055 each(className.split(' '), function() { 3056 self.toggleClass(this, state); 3057 }); 3058 } else { 3059 self.each(function(node) { 3060 var existingClassName; 3061 3062 if (hasClass(node, className) !== state) { 3063 existingClassName = node.className; 3064 3065 if (state) { 3066 node.className += existingClassName ? ' ' + className : className; 3067 } else { 3068 node.className = trim((" " + existingClassName + " ").replace(' ' + className + ' ', ' ')); 3069 } 3070 } 3071 }); 3072 } 3073 3074 return self; 3075 }, 3076 3077 hasClass: function(className) { 3078 return hasClass(this[0], className); 3079 }, 3080 3081 each: function(callback) { 3082 return each(this, callback); 3083 }, 3084 3085 on: function(name, callback) { 3086 return this.each(function() { 3087 Event.bind(this, name, callback); 3088 }); 3089 }, 3090 3091 off: function(name, callback) { 3092 return this.each(function() { 3093 Event.unbind(this, name, callback); 3094 }); 3095 }, 3096 3097 show: function() { 3098 return this.css('display', ''); 3099 }, 3100 3101 hide: function() { 3102 return this.css('display', 'none'); 3103 }, 3104 3105 slice: function() { 3106 return new DomQuery(slice.apply(this, arguments)); 3107 }, 3108 3109 eq: function(index) { 3110 return index === -1 ? this.slice(index) : this.slice(index, +index + 1); 3111 }, 3112 3113 first: function() { 3114 return this.eq(0); 3115 }, 3116 3117 last: function() { 3118 return this.eq(-1); 3119 }, 3120 3121 replaceWith: function(content) { 3122 var self = this; 3123 3124 if (self[0]) { 3125 self[0].parentNode.replaceChild(DomQuery(content)[0], self[0]); 3126 } 3127 3128 return self; 3129 }, 3130 3131 wrap: function(wrapper) { 3132 wrapper = DomQuery(wrapper)[0]; 3133 3134 return this.each(function() { 3135 var self = this, newWrapper = wrapper.cloneNode(false); 3136 self.parentNode.insertBefore(newWrapper, self); 3137 newWrapper.appendChild(self); 3138 }); 3139 }, 3140 3141 unwrap: function() { 3142 return this.each(function() { 3143 var self = this, node = self.firstChild, currentNode; 3144 3145 while (node) { 3146 currentNode = node; 3147 node = node.nextSibling; 3148 self.parentNode.insertBefore(currentNode, self); 3149 } 3150 }); 3151 }, 3152 3153 clone: function() { 3154 var result = []; 3155 3156 this.each(function() { 3157 result.push(this.cloneNode(true)); 3158 }); 3159 3160 return DomQuery(result); 3161 }, 3162 3163 find: function(selector) { 3164 var i, l, ret = []; 3165 3166 for (i = 0, l = this.length; i < l; i++) { 3167 DomQuery.find(selector, this[i], ret); 3168 } 3169 3170 return DomQuery(ret); 3171 }, 3172 3173 push: push, 3174 sort: [].sort, 3175 splice: [].splice 3176 }; 3177 3178 // Static members 3179 extend(DomQuery, { 3180 extend: extend, 3181 toArray: toArray, 3182 inArray: inArray, 3183 isArray: isArray, 3184 each: each, 3185 trim: trim, 3186 makeMap: makeMap, 3187 3188 // Sizzle 3189 find: Sizzle, 3190 expr: Sizzle.selectors, 3191 unique: Sizzle.uniqueSort, 3192 text: Sizzle.getText, 3193 isXMLDoc: Sizzle.isXML, 3194 contains: Sizzle.contains, 3195 filter: function(expr, elems, not) { 3196 if (not) { 3197 expr = ":not(" + expr + ")"; 3198 } 3199 3200 if (elems.length === 1) { 3201 elems = DomQuery.find.matchesSelector(elems[0], expr) ? [elems[0]] : []; 3202 } else { 3203 elems = DomQuery.find.matches(expr, elems); 3204 } 3205 3206 return elems; 3207 } 3208 }); 3209 3210 function dir(el, prop, until) { 3211 var matched = [], cur = el[prop]; 3212 3213 while (cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !DomQuery(cur).is(until))) { 3214 if (cur.nodeType === 1) { 3215 matched.push(cur); 3216 } 3217 3218 cur = cur[prop]; 3219 } 3220 3221 return matched; 3222 } 3223 3224 function sibling(n, el, siblingName, nodeType) { 3225 var r = []; 3226 3227 for(; n; n = n[siblingName]) { 3228 if ((!nodeType || n.nodeType === nodeType) && n !== el) { 3229 r.push(n); 3230 } 3231 } 3232 3233 return r; 3234 } 3235 3236 each({ 3237 parent: function(node) { 3238 var parent = node.parentNode; 3239 3240 return parent && parent.nodeType !== 11 ? parent : null; 3241 }, 3242 3243 parents: function(node) { 3244 return dir(node, "parentNode"); 3245 }, 3246 3247 parentsUntil: function(node, until) { 3248 return dir(node, "parentNode", until); 3249 }, 3250 3251 next: function(node) { 3252 return sibling(node, 'nextSibling', 1); 3253 }, 3254 3255 prev: function(node) { 3256 return sibling(node, 'previousSibling', 1); 3257 }, 3258 3259 nextNodes: function(node) { 3260 return sibling(node, 'nextSibling'); 3261 }, 3262 3263 prevNodes: function(node) { 3264 return sibling(node, 'previousSibling'); 3265 }, 3266 3267 children: function(node) { 3268 return sibling(node.firstChild, 'nextSibling', 1); 3269 }, 3270 3271 contents: function(node) { 3272 return toArray((node.nodeName === "iframe" ? node.contentDocument || node.contentWindow.document : node).childNodes); 3273 } 3274 }, function(name, fn){ 3275 DomQuery.fn[name] = function(selector) { 3276 var self = this, result; 3277 3278 if (self.length > 1) { 3279 throw new Error("DomQuery only supports traverse functions on a single node."); 3280 } 3281 3282 if (self[0]) { 3283 result = fn(self[0], selector); 3284 } 3285 3286 result = DomQuery(result); 3287 3288 if (selector && name !== "parentsUntil") { 3289 return result.filter(selector); 3290 } 3291 3292 return result; 3293 }; 3294 }); 3295 3296 DomQuery.fn.filter = function(selector) { 3297 return DomQuery.filter(selector); 3298 }; 3299 3300 DomQuery.fn.is = function(selector) { 3301 return !!selector && this.filter(selector).length > 0; 3302 }; 3303 3304 DomQuery.fn.init.prototype = DomQuery.fn; 3305 3306 return DomQuery; 3307 }); 3308 3309 // Included from: js/tinymce/classes/html/Styles.js 3310 3311 /** 3312 * Styles.js 3313 * 3314 * Copyright, Moxiecode Systems AB 3315 * Released under LGPL License. 3316 * 3317 * License: http://www.tinymce.com/license 3318 * Contributing: http://www.tinymce.com/contributing 3319 */ 3320 3321 /** 3322 * This class is used to parse CSS styles it also compresses styles to reduce the output size. 3323 * 3324 * @example 3325 * var Styles = new tinymce.html.Styles({ 3326 * url_converter: function(url) { 3327 * return url; 3328 * } 3329 * }); 3330 * 3331 * styles = Styles.parse('border: 1px solid red'); 3332 * styles.color = 'red'; 3333 * 3334 * console.log(new tinymce.html.StyleSerializer().serialize(styles)); 3335 * 3336 * @class tinymce.html.Styles 3337 * @version 3.4 3338 */ 3339 define("tinymce/html/Styles", [], function() { 3340 return function(settings, schema) { 3341 /*jshint maxlen:255 */ 3342 /*eslint max-len:0 */ 3343 var rgbRegExp = /rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)/gi, 3344 urlOrStrRegExp = /(?:url(?:(?:\(\s*\"([^\"]+)\"\s*\))|(?:\(\s*\'([^\']+)\'\s*\))|(?:\(\s*([^)\s]+)\s*\))))|(?:\'([^\']+)\')|(?:\"([^\"]+)\")/gi, 3345 styleRegExp = /\s*([^:]+):\s*([^;]+);?/g, 3346 trimRightRegExp = /\s+$/, 3347 undef, i, encodingLookup = {}, encodingItems, invisibleChar = '\uFEFF'; 3348 3349 settings = settings || {}; 3350 3351 encodingItems = ('\\" \\\' \\; \\: ; : ' + invisibleChar).split(' '); 3352 for (i = 0; i < encodingItems.length; i++) { 3353 encodingLookup[encodingItems[i]] = invisibleChar + i; 3354 encodingLookup[invisibleChar + i] = encodingItems[i]; 3355 } 3356 3357 function toHex(match, r, g, b) { 3358 function hex(val) { 3359 val = parseInt(val, 10).toString(16); 3360 3361 return val.length > 1 ? val : '0' + val; // 0 -> 00 3362 } 3363 3364 return '#' + hex(r) + hex(g) + hex(b); 3365 } 3366 3367 return { 3368 /** 3369 * Parses the specified RGB color value and returns a hex version of that color. 3370 * 3371 * @method toHex 3372 * @param {String} color RGB string value like rgb(1,2,3) 3373 * @return {String} Hex version of that RGB value like #FF00FF. 3374 */ 3375 toHex: function(color) { 3376 return color.replace(rgbRegExp, toHex); 3377 }, 3378 3379 /** 3380 * Parses the specified style value into an object collection. This parser will also 3381 * merge and remove any redundant items that browsers might have added. It will also convert non hex 3382 * colors to hex values. Urls inside the styles will also be converted to absolute/relative based on settings. 3383 * 3384 * @method parse 3385 * @param {String} css Style value to parse for example: border:1px solid red;. 3386 * @return {Object} Object representation of that style like {border: '1px solid red'} 3387 */ 3388 parse: function(css) { 3389 var styles = {}, matches, name, value, isEncoded, urlConverter = settings.url_converter; 3390 var urlConverterScope = settings.url_converter_scope || this; 3391 3392 function compress(prefix, suffix, noJoin) { 3393 var top, right, bottom, left; 3394 3395 top = styles[prefix + '-top' + suffix]; 3396 if (!top) { 3397 return; 3398 } 3399 3400 right = styles[prefix + '-right' + suffix]; 3401 if (!right) { 3402 return; 3403 } 3404 3405 bottom = styles[prefix + '-bottom' + suffix]; 3406 if (!bottom) { 3407 return; 3408 } 3409 3410 left = styles[prefix + '-left' + suffix]; 3411 if (!left) { 3412 return; 3413 } 3414 3415 var box = [top, right, bottom, left]; 3416 i = box.length - 1; 3417 while (i--) { 3418 if (box[i] !== box[i + 1]) { 3419 break; 3420 } 3421 } 3422 3423 if (i > -1 && noJoin) { 3424 return; 3425 } 3426 3427 styles[prefix + suffix] = i == -1 ? box[0] : box.join(' '); 3428 delete styles[prefix + '-top' + suffix]; 3429 delete styles[prefix + '-right' + suffix]; 3430 delete styles[prefix + '-bottom' + suffix]; 3431 delete styles[prefix + '-left' + suffix]; 3432 } 3433 3434 /** 3435 * Checks if the specific style can be compressed in other words if all border-width are equal. 3436 */ 3437 function canCompress(key) { 3438 var value = styles[key], i; 3439 3440 if (!value) { 3441 return; 3442 } 3443 3444 value = value.split(' '); 3445 i = value.length; 3446 while (i--) { 3447 if (value[i] !== value[0]) { 3448 return false; 3449 } 3450 } 3451 3452 styles[key] = value[0]; 3453 3454 return true; 3455 } 3456 3457 /** 3458 * Compresses multiple styles into one style. 3459 */ 3460 function compress2(target, a, b, c) { 3461 if (!canCompress(a)) { 3462 return; 3463 } 3464 3465 if (!canCompress(b)) { 3466 return; 3467 } 3468 3469 if (!canCompress(c)) { 3470 return; 3471 } 3472 3473 // Compress 3474 styles[target] = styles[a] + ' ' + styles[b] + ' ' + styles[c]; 3475 delete styles[a]; 3476 delete styles[b]; 3477 delete styles[c]; 3478 } 3479 3480 // Encodes the specified string by replacing all \" \' ; : with _<num> 3481 function encode(str) { 3482 isEncoded = true; 3483 3484 return encodingLookup[str]; 3485 } 3486 3487 // Decodes the specified string by replacing all _<num> with it's original value \" \' etc 3488 // It will also decode the \" \' if keep_slashes is set to fale or omitted 3489 function decode(str, keep_slashes) { 3490 if (isEncoded) { 3491 str = str.replace(/\uFEFF[0-9]/g, function(str) { 3492 return encodingLookup[str]; 3493 }); 3494 } 3495 3496 if (!keep_slashes) { 3497 str = str.replace(/\\([\'\";:])/g, "$1"); 3498 } 3499 3500 return str; 3501 } 3502 3503 function processUrl(match, url, url2, url3, str, str2) { 3504 str = str || str2; 3505 3506 if (str) { 3507 str = decode(str); 3508 3509 // Force strings into single quote format 3510 return "'" + str.replace(/\'/g, "\\'") + "'"; 3511 } 3512 3513 url = decode(url || url2 || url3); 3514 3515 if (!settings.allow_script_urls) { 3516 var scriptUrl = url.replace(/[\s\r\n]+/, ''); 3517 3518 if (/(java|vb)script:/i.test(scriptUrl)) { 3519 return ""; 3520 } 3521 3522 if (!settings.allow_svg_data_urls && /^data:image\/svg/i.test(scriptUrl)) { 3523 return ""; 3524 } 3525 } 3526 3527 // Convert the URL to relative/absolute depending on config 3528 if (urlConverter) { 3529 url = urlConverter.call(urlConverterScope, url, 'style'); 3530 } 3531 3532 // Output new URL format 3533 return "url('" + url.replace(/\'/g, "\\'") + "')"; 3534 } 3535 3536 if (css) { 3537 css = css.replace(/[\u0000-\u001F]/g, ''); 3538 3539 // Encode \" \' % and ; and : inside strings so they don't interfere with the style parsing 3540 css = css.replace(/\\[\"\';:\uFEFF]/g, encode).replace(/\"[^\"]+\"|\'[^\']+\'/g, function(str) { 3541 return str.replace(/[;:]/g, encode); 3542 }); 3543 3544 // Parse styles 3545 while ((matches = styleRegExp.exec(css))) { 3546 name = matches[1].replace(trimRightRegExp, '').toLowerCase(); 3547 value = matches[2].replace(trimRightRegExp, ''); 3548 3549 // Decode escaped sequences like \65 -> e 3550 /*jshint loopfunc:true*/ 3551 /*eslint no-loop-func:0 */ 3552 value = value.replace(/\\[0-9a-f]+/g, function(e) { 3553 return String.fromCharCode(parseInt(e.substr(1), 16)); 3554 }); 3555 3556 if (name && value.length > 0) { 3557 // Don't allow behavior name or expression/comments within the values 3558 if (!settings.allow_script_urls && (name == "behavior" || /expression\s*\(|\/\*|\*\//.test(value))) { 3559 continue; 3560 } 3561 3562 // Opera will produce 700 instead of bold in their style values 3563 if (name === 'font-weight' && value === '700') { 3564 value = 'bold'; 3565 } else if (name === 'color' || name === 'background-color') { // Lowercase colors like RED 3566 value = value.toLowerCase(); 3567 } 3568 3569 // Convert RGB colors to HEX 3570 value = value.replace(rgbRegExp, toHex); 3571 3572 // Convert URLs and force them into url('value') format 3573 value = value.replace(urlOrStrRegExp, processUrl); 3574 styles[name] = isEncoded ? decode(value, true) : value; 3575 } 3576 3577 styleRegExp.lastIndex = matches.index + matches[0].length; 3578 } 3579 // Compress the styles to reduce it's size for example IE will expand styles 3580 compress("border", "", true); 3581 compress("border", "-width"); 3582 compress("border", "-color"); 3583 compress("border", "-style"); 3584 compress("padding", ""); 3585 compress("margin", ""); 3586 compress2('border', 'border-width', 'border-style', 'border-color'); 3587 3588 // Remove pointless border, IE produces these 3589 if (styles.border === 'medium none') { 3590 delete styles.border; 3591 } 3592 3593 // IE 11 will produce a border-image: none when getting the style attribute from <p style="border: 1px solid red"></p> 3594 // So lets asume it shouldn't be there 3595 if (styles['border-image'] === 'none') { 3596 delete styles['border-image']; 3597 } 3598 } 3599 3600 return styles; 3601 }, 3602 3603 /** 3604 * Serializes the specified style object into a string. 3605 * 3606 * @method serialize 3607 * @param {Object} styles Object to serialize as string for example: {border: '1px solid red'} 3608 * @param {String} element_name Optional element name, if specified only the styles that matches the schema will be serialized. 3609 * @return {String} String representation of the style object for example: border: 1px solid red. 3610 */ 3611 serialize: function(styles, element_name) { 3612 var css = '', name, value; 3613 3614 function serializeStyles(name) { 3615 var styleList, i, l, value; 3616 3617 styleList = schema.styles[name]; 3618 if (styleList) { 3619 for (i = 0, l = styleList.length; i < l; i++) { 3620 name = styleList[i]; 3621 value = styles[name]; 3622 3623 if (value !== undef && value.length > 0) { 3624 css += (css.length > 0 ? ' ' : '') + name + ': ' + value + ';'; 3625 } 3626 } 3627 } 3628 } 3629 3630 // Serialize styles according to schema 3631 if (element_name && schema && schema.styles) { 3632 // Serialize global styles and element specific styles 3633 serializeStyles('*'); 3634 serializeStyles(element_name); 3635 } else { 3636 // Output the styles in the order they are inside the object 3637 for (name in styles) { 3638 value = styles[name]; 3639 3640 if (value !== undef && value.length > 0) { 3641 css += (css.length > 0 ? ' ' : '') + name + ': ' + value + ';'; 3642 } 3643 } 3644 } 3645 3646 return css; 3647 } 3648 }; 3649 }; 3650 }); 3651 3652 // Included from: js/tinymce/classes/dom/TreeWalker.js 3653 3654 /** 3655 * TreeWalker.js 3656 * 3657 * Copyright, Moxiecode Systems AB 3658 * Released under LGPL License. 3659 * 3660 * License: http://www.tinymce.com/license 3661 * Contributing: http://www.tinymce.com/contributing 3662 */ 3663 3664 /** 3665 * TreeWalker class enables you to walk the DOM in a linear manner. 3666 * 3667 * @class tinymce.dom.TreeWalker 3668 */ 3669 define("tinymce/dom/TreeWalker", [], function() { 3670 return function(start_node, root_node) { 3671 var node = start_node; 3672 3673 function findSibling(node, start_name, sibling_name, shallow) { 3674 var sibling, parent; 3675 3676 if (node) { 3677 // Walk into nodes if it has a start 3678 if (!shallow && node[start_name]) { 3679 return node[start_name]; 3680 } 3681 3682 // Return the sibling if it has one 3683 if (node != root_node) { 3684 sibling = node[sibling_name]; 3685 if (sibling) { 3686 return sibling; 3687 } 3688 3689 // Walk up the parents to look for siblings 3690 for (parent = node.parentNode; parent && parent != root_node; parent = parent.parentNode) { 3691 sibling = parent[sibling_name]; 3692 if (sibling) { 3693 return sibling; 3694 } 3695 } 3696 } 3697 } 3698 } 3699 3700 /** 3701 * Returns the current node. 3702 * 3703 * @method current 3704 * @return {Node} Current node where the walker is. 3705 */ 3706 this.current = function() { 3707 return node; 3708 }; 3709 3710 /** 3711 * Walks to the next node in tree. 3712 * 3713 * @method next 3714 * @return {Node} Current node where the walker is after moving to the next node. 3715 */ 3716 this.next = function(shallow) { 3717 node = findSibling(node, 'firstChild', 'nextSibling', shallow); 3718 return node; 3719 }; 3720 3721 /** 3722 * Walks to the previous node in tree. 3723 * 3724 * @method prev 3725 * @return {Node} Current node where the walker is after moving to the previous node. 3726 */ 3727 this.prev = function(shallow) { 3728 node = findSibling(node, 'lastChild', 'previousSibling', shallow); 3729 return node; 3730 }; 3731 }; 3732 }); 3733 3734 // Included from: js/tinymce/classes/util/Tools.js 3735 3736 /** 3737 * Tools.js 3738 * 3739 * Copyright, Moxiecode Systems AB 3740 * Released under LGPL License. 3741 * 3742 * License: http://www.tinymce.com/license 3743 * Contributing: http://www.tinymce.com/contributing 3744 */ 3745 3746 /** 3747 * This class contains various utlity functions. These are also exposed 3748 * directly on the tinymce namespace. 3749 * 3750 * @class tinymce.util.Tools 3751 */ 3752 define("tinymce/util/Tools", [], function() { 3753 /** 3754 * Removes whitespace from the beginning and end of a string. 3755 * 3756 * @method trim 3757 * @param {String} s String to remove whitespace from. 3758 * @return {String} New string with removed whitespace. 3759 */ 3760 var whiteSpaceRegExp = /^\s*|\s*$/g; 3761 3762 function trim(str) { 3763 return (str === null || str === undefined) ? '' : ("" + str).replace(whiteSpaceRegExp, ''); 3764 } 3765 3766 /** 3767 * Returns true/false if the object is an array or not. 3768 * 3769 * @method isArray 3770 * @param {Object} obj Object to check. 3771 * @return {boolean} true/false state if the object is an array or not. 3772 */ 3773 var isArray = Array.isArray || function(obj) { 3774 return Object.prototype.toString.call(obj) === "[object Array]"; 3775 }; 3776 3777 /** 3778 * Checks if a object is of a specific type for example an array. 3779 * 3780 * @method is 3781 * @param {Object} o Object to check type of. 3782 * @param {string} t Optional type to check for. 3783 * @return {Boolean} true/false if the object is of the specified type. 3784 */ 3785 function is(o, t) { 3786 if (!t) { 3787 return o !== undefined; 3788 } 3789 3790 if (t == 'array' && isArray(o)) { 3791 return true; 3792 } 3793 3794 return typeof(o) == t; 3795 } 3796 3797 /** 3798 * Converts the specified object into a real JavaScript array. 3799 * 3800 * @method toArray 3801 * @param {Object} obj Object to convert into array. 3802 * @return {Array} Array object based in input. 3803 */ 3804 function toArray(obj) { 3805 var array = [], i, l; 3806 3807 for (i = 0, l = obj.length; i < l; i++) { 3808 array[i] = obj[i]; 3809 } 3810 3811 return array; 3812 } 3813 3814 /** 3815 * Makes a name/object map out of an array with names. 3816 * 3817 * @method makeMap 3818 * @param {Array/String} items Items to make map out of. 3819 * @param {String} delim Optional delimiter to split string by. 3820 * @param {Object} map Optional map to add items to. 3821 * @return {Object} Name/value map of items. 3822 */ 3823 function makeMap(items, delim, map) { 3824 var i; 3825 3826 items = items || []; 3827 delim = delim || ','; 3828 3829 if (typeof(items) == "string") { 3830 items = items.split(delim); 3831 } 3832 3833 map = map || {}; 3834 3835 i = items.length; 3836 while (i--) { 3837 map[items[i]] = {}; 3838 } 3839 3840 return map; 3841 } 3842 3843 /** 3844 * Performs an iteration of all items in a collection such as an object or array. This method will execure the 3845 * callback function for each item in the collection, if the callback returns false the iteration will terminate. 3846 * The callback has the following format: cb(value, key_or_index). 3847 * 3848 * @method each 3849 * @param {Object} o Collection to iterate. 3850 * @param {function} cb Callback function to execute for each item. 3851 * @param {Object} s Optional scope to execute the callback in. 3852 * @example 3853 * // Iterate an array 3854 * tinymce.each([1,2,3], function(v, i) { 3855 * console.debug("Value: " + v + ", Index: " + i); 3856 * }); 3857 * 3858 * // Iterate an object 3859 * tinymce.each({a: 1, b: 2, c: 3], function(v, k) { 3860 * console.debug("Value: " + v + ", Key: " + k); 3861 * }); 3862 */ 3863 function each(o, cb, s) { 3864 var n, l; 3865 3866 if (!o) { 3867 return 0; 3868 } 3869 3870 s = s || o; 3871 3872 if (o.length !== undefined) { 3873 // Indexed arrays, needed for Safari 3874 for (n = 0, l = o.length; n < l; n++) { 3875 if (cb.call(s, o[n], n, o) === false) { 3876 return 0; 3877 } 3878 } 3879 } else { 3880 // Hashtables 3881 for (n in o) { 3882 if (o.hasOwnProperty(n)) { 3883 if (cb.call(s, o[n], n, o) === false) { 3884 return 0; 3885 } 3886 } 3887 } 3888 } 3889 3890 return 1; 3891 } 3892 3893 /** 3894 * Creates a new array by the return value of each iteration function call. This enables you to convert 3895 * one array list into another. 3896 * 3897 * @method map 3898 * @param {Array} a Array of items to iterate. 3899 * @param {function} f Function to call for each item. It's return value will be the new value. 3900 * @return {Array} Array with new values based on function return values. 3901 */ 3902 function map(a, f) { 3903 var o = []; 3904 3905 each(a, function(v) { 3906 o.push(f(v)); 3907 }); 3908 3909 return o; 3910 } 3911 3912 /** 3913 * Filters out items from the input array by calling the specified function for each item. 3914 * If the function returns false the item will be excluded if it returns true it will be included. 3915 * 3916 * @method grep 3917 * @param {Array} a Array of items to loop though. 3918 * @param {function} f Function to call for each item. Include/exclude depends on it's return value. 3919 * @return {Array} New array with values imported and filtered based in input. 3920 * @example 3921 * // Filter out some items, this will return an array with 4 and 5 3922 * var items = tinymce.grep([1,2,3,4,5], function(v) {return v > 3;}); 3923 */ 3924 function grep(a, f) { 3925 var o = []; 3926 3927 each(a, function(v) { 3928 if (!f || f(v)) { 3929 o.push(v); 3930 } 3931 }); 3932 3933 return o; 3934 } 3935 3936 /** 3937 * Creates a class, subclass or static singleton. 3938 * More details on this method can be found in the Wiki. 3939 * 3940 * @method create 3941 * @param {String} s Class name, inheritage and prefix. 3942 * @param {Object} p Collection of methods to add to the class. 3943 * @param {Object} root Optional root object defaults to the global window object. 3944 * @example 3945 * // Creates a basic class 3946 * tinymce.create('tinymce.somepackage.SomeClass', { 3947 * SomeClass: function() { 3948 * // Class constructor 3949 * }, 3950 * 3951 * method: function() { 3952 * // Some method 3953 * } 3954 * }); 3955 * 3956 * // Creates a basic subclass class 3957 * tinymce.create('tinymce.somepackage.SomeSubClass:tinymce.somepackage.SomeClass', { 3958 * SomeSubClass: function() { 3959 * // Class constructor 3960 * this.parent(); // Call parent constructor 3961 * }, 3962 * 3963 * method: function() { 3964 * // Some method 3965 * this.parent(); // Call parent method 3966 * }, 3967 * 3968 * 'static': { 3969 * staticMethod: function() { 3970 * // Static method 3971 * } 3972 * } 3973 * }); 3974 * 3975 * // Creates a singleton/static class 3976 * tinymce.create('static tinymce.somepackage.SomeSingletonClass', { 3977 * method: function() { 3978 * // Some method 3979 * } 3980 * }); 3981 */ 3982 function create(s, p, root) { 3983 var self = this, sp, ns, cn, scn, c, de = 0; 3984 3985 // Parse : <prefix> <class>:<super class> 3986 s = /^((static) )?([\w.]+)(:([\w.]+))?/.exec(s); 3987 cn = s[3].match(/(^|\.)(\w+)$/i)[2]; // Class name 3988 3989 // Create namespace for new class 3990 ns = self.createNS(s[3].replace(/\.\w+$/, ''), root); 3991 3992 // Class already exists 3993 if (ns[cn]) { 3994 return; 3995 } 3996 3997 // Make pure static class 3998 if (s[2] == 'static') { 3999 ns[cn] = p; 4000 4001 if (this.onCreate) { 4002 this.onCreate(s[2], s[3], ns[cn]); 4003 } 4004 4005 return; 4006 } 4007 4008 // Create default constructor 4009 if (!p[cn]) { 4010 p[cn] = function() {}; 4011 de = 1; 4012 } 4013 4014 // Add constructor and methods 4015 ns[cn] = p[cn]; 4016 self.extend(ns[cn].prototype, p); 4017 4018 // Extend 4019 if (s[5]) { 4020 sp = self.resolve(s[5]).prototype; 4021 scn = s[5].match(/\.(\w+)$/i)[1]; // Class name 4022 4023 // Extend constructor 4024 c = ns[cn]; 4025 if (de) { 4026 // Add passthrough constructor 4027 ns[cn] = function() { 4028 return sp[scn].apply(this, arguments); 4029 }; 4030 } else { 4031 // Add inherit constructor 4032 ns[cn] = function() { 4033 this.parent = sp[scn]; 4034 return c.apply(this, arguments); 4035 }; 4036 } 4037 ns[cn].prototype[cn] = ns[cn]; 4038 4039 // Add super methods 4040 self.each(sp, function(f, n) { 4041 ns[cn].prototype[n] = sp[n]; 4042 }); 4043 4044 // Add overridden methods 4045 self.each(p, function(f, n) { 4046 // Extend methods if needed 4047 if (sp[n]) { 4048 ns[cn].prototype[n] = function() { 4049 this.parent = sp[n]; 4050 return f.apply(this, arguments); 4051 }; 4052 } else { 4053 if (n != cn) { 4054 ns[cn].prototype[n] = f; 4055 } 4056 } 4057 }); 4058 } 4059 4060 // Add static methods 4061 /*jshint sub:true*/ 4062 self.each(p['static'], function(f, n) { 4063 ns[cn][n] = f; 4064 }); 4065 } 4066 4067 /** 4068 * Returns the index of a value in an array, this method will return -1 if the item wasn't found. 4069 * 4070 * @method inArray 4071 * @param {Array} a Array/Object to search for value in. 4072 * @param {Object} v Value to check for inside the array. 4073 * @return {Number/String} Index of item inside the array inside an object. Or -1 if it wasn't found. 4074 * @example 4075 * // Get index of value in array this will alert 1 since 2 is at that index 4076 * alert(tinymce.inArray([1,2,3], 2)); 4077 */ 4078 function inArray(a, v) { 4079 var i, l; 4080 4081 if (a) { 4082 for (i = 0, l = a.length; i < l; i++) { 4083 if (a[i] === v) { 4084 return i; 4085 } 4086 } 4087 } 4088 4089 return -1; 4090 } 4091 4092 function extend(obj, ext) { 4093 var i, l, name, args = arguments, value; 4094 4095 for (i = 1, l = args.length; i < l; i++) { 4096 ext = args[i]; 4097 for (name in ext) { 4098 if (ext.hasOwnProperty(name)) { 4099 value = ext[name]; 4100 4101 if (value !== undefined) { 4102 obj[name] = value; 4103 } 4104 } 4105 } 4106 } 4107 4108 return obj; 4109 } 4110 4111 /** 4112 * Executed the specified function for each item in a object tree. 4113 * 4114 * @method walk 4115 * @param {Object} o Object tree to walk though. 4116 * @param {function} f Function to call for each item. 4117 * @param {String} n Optional name of collection inside the objects to walk for example childNodes. 4118 * @param {String} s Optional scope to execute the function in. 4119 */ 4120 function walk(o, f, n, s) { 4121 s = s || this; 4122 4123 if (o) { 4124 if (n) { 4125 o = o[n]; 4126 } 4127 4128 each(o, function(o, i) { 4129 if (f.call(s, o, i, n) === false) { 4130 return false; 4131 } 4132 4133 walk(o, f, n, s); 4134 }); 4135 } 4136 } 4137 4138 /** 4139 * Creates a namespace on a specific object. 4140 * 4141 * @method createNS 4142 * @param {String} n Namespace to create for example a.b.c.d. 4143 * @param {Object} o Optional object to add namespace to, defaults to window. 4144 * @return {Object} New namespace object the last item in path. 4145 * @example 4146 * // Create some namespace 4147 * tinymce.createNS('tinymce.somepackage.subpackage'); 4148 * 4149 * // Add a singleton 4150 * var tinymce.somepackage.subpackage.SomeSingleton = { 4151 * method: function() { 4152 * // Some method 4153 * } 4154 * }; 4155 */ 4156 function createNS(n, o) { 4157 var i, v; 4158 4159 o = o || window; 4160 4161 n = n.split('.'); 4162 for (i = 0; i < n.length; i++) { 4163 v = n[i]; 4164 4165 if (!o[v]) { 4166 o[v] = {}; 4167 } 4168 4169 o = o[v]; 4170 } 4171 4172 return o; 4173 } 4174 4175 /** 4176 * Resolves a string and returns the object from a specific structure. 4177 * 4178 * @method resolve 4179 * @param {String} n Path to resolve for example a.b.c.d. 4180 * @param {Object} o Optional object to search though, defaults to window. 4181 * @return {Object} Last object in path or null if it couldn't be resolved. 4182 * @example 4183 * // Resolve a path into an object reference 4184 * var obj = tinymce.resolve('a.b.c.d'); 4185 */ 4186 function resolve(n, o) { 4187 var i, l; 4188 4189 o = o || window; 4190 4191 n = n.split('.'); 4192 for (i = 0, l = n.length; i < l; i++) { 4193 o = o[n[i]]; 4194 4195 if (!o) { 4196 break; 4197 } 4198 } 4199 4200 return o; 4201 } 4202 4203 /** 4204 * Splits a string but removes the whitespace before and after each value. 4205 * 4206 * @method explode 4207 * @param {string} s String to split. 4208 * @param {string} d Delimiter to split by. 4209 * @example 4210 * // Split a string into an array with a,b,c 4211 * var arr = tinymce.explode('a, b, c'); 4212 */ 4213 function explode(s, d) { 4214 if (!s || is(s, 'array')) { 4215 return s; 4216 } 4217 4218 return map(s.split(d || ','), trim); 4219 } 4220 4221 return { 4222 trim: trim, 4223 isArray: isArray, 4224 is: is, 4225 toArray: toArray, 4226 makeMap: makeMap, 4227 each: each, 4228 map: map, 4229 grep: grep, 4230 inArray: inArray, 4231 extend: extend, 4232 create: create, 4233 walk: walk, 4234 createNS: createNS, 4235 resolve: resolve, 4236 explode: explode 4237 }; 4238 }); 4239 4240 // Included from: js/tinymce/classes/dom/Range.js 4241 4242 /** 4243 * Range.js 4244 * 4245 * Copyright, Moxiecode Systems AB 4246 * Released under LGPL License. 4247 * 4248 * License: http://www.tinymce.com/license 4249 * Contributing: http://www.tinymce.com/contributing 4250 */ 4251 4252 define("tinymce/dom/Range", [ 4253 "tinymce/util/Tools" 4254 ], function(Tools) { 4255 // Range constructor 4256 function Range(dom) { 4257 var self = this, 4258 doc = dom.doc, 4259 EXTRACT = 0, 4260 CLONE = 1, 4261 DELETE = 2, 4262 TRUE = true, 4263 FALSE = false, 4264 START_OFFSET = 'startOffset', 4265 START_CONTAINER = 'startContainer', 4266 END_CONTAINER = 'endContainer', 4267 END_OFFSET = 'endOffset', 4268 extend = Tools.extend, 4269 nodeIndex = dom.nodeIndex; 4270 4271 function createDocumentFragment() { 4272 return doc.createDocumentFragment(); 4273 } 4274 4275 function setStart(n, o) { 4276 _setEndPoint(TRUE, n, o); 4277 } 4278 4279 function setEnd(n, o) { 4280 _setEndPoint(FALSE, n, o); 4281 } 4282 4283 function setStartBefore(n) { 4284 setStart(n.parentNode, nodeIndex(n)); 4285 } 4286 4287 function setStartAfter(n) { 4288 setStart(n.parentNode, nodeIndex(n) + 1); 4289 } 4290 4291 function setEndBefore(n) { 4292 setEnd(n.parentNode, nodeIndex(n)); 4293 } 4294 4295 function setEndAfter(n) { 4296 setEnd(n.parentNode, nodeIndex(n) + 1); 4297 } 4298 4299 function collapse(ts) { 4300 if (ts) { 4301 self[END_CONTAINER] = self[START_CONTAINER]; 4302 self[END_OFFSET] = self[START_OFFSET]; 4303 } else { 4304 self[START_CONTAINER] = self[END_CONTAINER]; 4305 self[START_OFFSET] = self[END_OFFSET]; 4306 } 4307 4308 self.collapsed = TRUE; 4309 } 4310 4311 function selectNode(n) { 4312 setStartBefore(n); 4313 setEndAfter(n); 4314 } 4315 4316 function selectNodeContents(n) { 4317 setStart(n, 0); 4318 setEnd(n, n.nodeType === 1 ? n.childNodes.length : n.nodeValue.length); 4319 } 4320 4321 function compareBoundaryPoints(h, r) { 4322 var sc = self[START_CONTAINER], so = self[START_OFFSET], ec = self[END_CONTAINER], eo = self[END_OFFSET], 4323 rsc = r.startContainer, rso = r.startOffset, rec = r.endContainer, reo = r.endOffset; 4324 4325 // Check START_TO_START 4326 if (h === 0) { 4327 return _compareBoundaryPoints(sc, so, rsc, rso); 4328 } 4329 4330 // Check START_TO_END 4331 if (h === 1) { 4332 return _compareBoundaryPoints(ec, eo, rsc, rso); 4333 } 4334 4335 // Check END_TO_END 4336 if (h === 2) { 4337 return _compareBoundaryPoints(ec, eo, rec, reo); 4338 } 4339 4340 // Check END_TO_START 4341 if (h === 3) { 4342 return _compareBoundaryPoints(sc, so, rec, reo); 4343 } 4344 } 4345 4346 function deleteContents() { 4347 _traverse(DELETE); 4348 } 4349 4350 function extractContents() { 4351 return _traverse(EXTRACT); 4352 } 4353 4354 function cloneContents() { 4355 return _traverse(CLONE); 4356 } 4357 4358 function insertNode(n) { 4359 var startContainer = this[START_CONTAINER], 4360 startOffset = this[START_OFFSET], nn, o; 4361 4362 // Node is TEXT_NODE or CDATA 4363 if ((startContainer.nodeType === 3 || startContainer.nodeType === 4) && startContainer.nodeValue) { 4364 if (!startOffset) { 4365 // At the start of text 4366 startContainer.parentNode.insertBefore(n, startContainer); 4367 } else if (startOffset >= startContainer.nodeValue.length) { 4368 // At the end of text 4369 dom.insertAfter(n, startContainer); 4370 } else { 4371 // Middle, need to split 4372 nn = startContainer.splitText(startOffset); 4373 startContainer.parentNode.insertBefore(n, nn); 4374 } 4375 } else { 4376 // Insert element node 4377 if (startContainer.childNodes.length > 0) { 4378 o = startContainer.childNodes[startOffset]; 4379 } 4380 4381 if (o) { 4382 startContainer.insertBefore(n, o); 4383 } else { 4384 if (startContainer.nodeType == 3) { 4385 dom.insertAfter(n, startContainer); 4386 } else { 4387 startContainer.appendChild(n); 4388 } 4389 } 4390 } 4391 } 4392 4393 function surroundContents(n) { 4394 var f = self.extractContents(); 4395 4396 self.insertNode(n); 4397 n.appendChild(f); 4398 self.selectNode(n); 4399 } 4400 4401 function cloneRange() { 4402 return extend(new Range(dom), { 4403 startContainer: self[START_CONTAINER], 4404 startOffset: self[START_OFFSET], 4405 endContainer: self[END_CONTAINER], 4406 endOffset: self[END_OFFSET], 4407 collapsed: self.collapsed, 4408 commonAncestorContainer: self.commonAncestorContainer 4409 }); 4410 } 4411 4412 // Private methods 4413 4414 function _getSelectedNode(container, offset) { 4415 var child; 4416 4417 if (container.nodeType == 3 /* TEXT_NODE */) { 4418 return container; 4419 } 4420 4421 if (offset < 0) { 4422 return container; 4423 } 4424 4425 child = container.firstChild; 4426 while (child && offset > 0) { 4427 --offset; 4428 child = child.nextSibling; 4429 } 4430 4431 if (child) { 4432 return child; 4433 } 4434 4435 return container; 4436 } 4437 4438 function _isCollapsed() { 4439 return (self[START_CONTAINER] == self[END_CONTAINER] && self[START_OFFSET] == self[END_OFFSET]); 4440 } 4441 4442 function _compareBoundaryPoints(containerA, offsetA, containerB, offsetB) { 4443 var c, offsetC, n, cmnRoot, childA, childB; 4444 4445 // In the first case the boundary-points have the same container. A is before B 4446 // if its offset is less than the offset of B, A is equal to B if its offset is 4447 // equal to the offset of B, and A is after B if its offset is greater than the 4448 // offset of B. 4449 if (containerA == containerB) { 4450 if (offsetA == offsetB) { 4451 return 0; // equal 4452 } 4453 4454 if (offsetA < offsetB) { 4455 return -1; // before 4456 } 4457 4458 return 1; // after 4459 } 4460 4461 // In the second case a child node C of the container of A is an ancestor 4462 // container of B. In this case, A is before B if the offset of A is less than or 4463 // equal to the index of the child node C and A is after B otherwise. 4464 c = containerB; 4465 while (c && c.parentNode != containerA) { 4466 c = c.parentNode; 4467 } 4468 4469 if (c) { 4470 offsetC = 0; 4471 n = containerA.firstChild; 4472 4473 while (n != c && offsetC < offsetA) { 4474 offsetC++; 4475 n = n.nextSibling; 4476 } 4477 4478 if (offsetA <= offsetC) { 4479 return -1; // before 4480 } 4481 4482 return 1; // after 4483 } 4484 4485 // In the third case a child node C of the container of B is an ancestor container 4486 // of A. In this case, A is before B if the index of the child node C is less than 4487 // the offset of B and A is after B otherwise. 4488 c = containerA; 4489 while (c && c.parentNode != containerB) { 4490 c = c.parentNode; 4491 } 4492 4493 if (c) { 4494 offsetC = 0; 4495 n = containerB.firstChild; 4496 4497 while (n != c && offsetC < offsetB) { 4498 offsetC++; 4499 n = n.nextSibling; 4500 } 4501 4502 if (offsetC < offsetB) { 4503 return -1; // before 4504 } 4505 4506 return 1; // after 4507 } 4508 4509 // In the fourth case, none of three other cases hold: the containers of A and B 4510 // are siblings or descendants of sibling nodes. In this case, A is before B if 4511 // the container of A is before the container of B in a pre-order traversal of the 4512 // Ranges' context tree and A is after B otherwise. 4513 cmnRoot = dom.findCommonAncestor(containerA, containerB); 4514 childA = containerA; 4515 4516 while (childA && childA.parentNode != cmnRoot) { 4517 childA = childA.parentNode; 4518 } 4519 4520 if (!childA) { 4521 childA = cmnRoot; 4522 } 4523 4524 childB = containerB; 4525 while (childB && childB.parentNode != cmnRoot) { 4526 childB = childB.parentNode; 4527 } 4528 4529 if (!childB) { 4530 childB = cmnRoot; 4531 } 4532 4533 if (childA == childB) { 4534 return 0; // equal 4535 } 4536 4537 n = cmnRoot.firstChild; 4538 while (n) { 4539 if (n == childA) { 4540 return -1; // before 4541 } 4542 4543 if (n == childB) { 4544 return 1; // after 4545 } 4546 4547 n = n.nextSibling; 4548 } 4549 } 4550 4551 function _setEndPoint(st, n, o) { 4552 var ec, sc; 4553 4554 if (st) { 4555 self[START_CONTAINER] = n; 4556 self[START_OFFSET] = o; 4557 } else { 4558 self[END_CONTAINER] = n; 4559 self[END_OFFSET] = o; 4560 } 4561 4562 // If one boundary-point of a Range is set to have a root container 4563 // other than the current one for the Range, the Range is collapsed to 4564 // the new position. This enforces the restriction that both boundary- 4565 // points of a Range must have the same root container. 4566 ec = self[END_CONTAINER]; 4567 while (ec.parentNode) { 4568 ec = ec.parentNode; 4569 } 4570 4571 sc = self[START_CONTAINER]; 4572 while (sc.parentNode) { 4573 sc = sc.parentNode; 4574 } 4575 4576 if (sc == ec) { 4577 // The start position of a Range is guaranteed to never be after the 4578 // end position. To enforce this restriction, if the start is set to 4579 // be at a position after the end, the Range is collapsed to that 4580 // position. 4581 if (_compareBoundaryPoints(self[START_CONTAINER], self[START_OFFSET], self[END_CONTAINER], self[END_OFFSET]) > 0) { 4582 self.collapse(st); 4583 } 4584 } else { 4585 self.collapse(st); 4586 } 4587 4588 self.collapsed = _isCollapsed(); 4589 self.commonAncestorContainer = dom.findCommonAncestor(self[START_CONTAINER], self[END_CONTAINER]); 4590 } 4591 4592 function _traverse(how) { 4593 var c, endContainerDepth = 0, startContainerDepth = 0, p, depthDiff, startNode, endNode, sp, ep; 4594 4595 if (self[START_CONTAINER] == self[END_CONTAINER]) { 4596 return _traverseSameContainer(how); 4597 } 4598 4599 for (c = self[END_CONTAINER], p = c.parentNode; p; c = p, p = p.parentNode) { 4600 if (p == self[START_CONTAINER]) { 4601 return _traverseCommonStartContainer(c, how); 4602 } 4603 4604 ++endContainerDepth; 4605 } 4606 4607 for (c = self[START_CONTAINER], p = c.parentNode; p; c = p, p = p.parentNode) { 4608 if (p == self[END_CONTAINER]) { 4609 return _traverseCommonEndContainer(c, how); 4610 } 4611 4612 ++startContainerDepth; 4613 } 4614 4615 depthDiff = startContainerDepth - endContainerDepth; 4616 4617 startNode = self[START_CONTAINER]; 4618 while (depthDiff > 0) { 4619 startNode = startNode.parentNode; 4620 depthDiff--; 4621 } 4622 4623 endNode = self[END_CONTAINER]; 4624 while (depthDiff < 0) { 4625 endNode = endNode.parentNode; 4626 depthDiff++; 4627 } 4628 4629 // ascend the ancestor hierarchy until we have a common parent. 4630 for (sp = startNode.parentNode, ep = endNode.parentNode; sp != ep; sp = sp.parentNode, ep = ep.parentNode) { 4631 startNode = sp; 4632 endNode = ep; 4633 } 4634 4635 return _traverseCommonAncestors(startNode, endNode, how); 4636 } 4637 4638 function _traverseSameContainer(how) { 4639 var frag, s, sub, n, cnt, sibling, xferNode, start, len; 4640 4641 if (how != DELETE) { 4642 frag = createDocumentFragment(); 4643 } 4644 4645 // If selection is empty, just return the fragment 4646 if (self[START_OFFSET] == self[END_OFFSET]) { 4647 return frag; 4648 } 4649 4650 // Text node needs special case handling 4651 if (self[START_CONTAINER].nodeType == 3 /* TEXT_NODE */) { 4652 // get the substring 4653 s = self[START_CONTAINER].nodeValue; 4654 sub = s.substring(self[START_OFFSET], self[END_OFFSET]); 4655 4656 // set the original text node to its new value 4657 if (how != CLONE) { 4658 n = self[START_CONTAINER]; 4659 start = self[START_OFFSET]; 4660 len = self[END_OFFSET] - self[START_OFFSET]; 4661 4662 if (start === 0 && len >= n.nodeValue.length - 1) { 4663 n.parentNode.removeChild(n); 4664 } else { 4665 n.deleteData(start, len); 4666 } 4667 4668 // Nothing is partially selected, so collapse to start point 4669 self.collapse(TRUE); 4670 } 4671 4672 if (how == DELETE) { 4673 return; 4674 } 4675 4676 if (sub.length > 0) { 4677 frag.appendChild(doc.createTextNode(sub)); 4678 } 4679 4680 return frag; 4681 } 4682 4683 // Copy nodes between the start/end offsets. 4684 n = _getSelectedNode(self[START_CONTAINER], self[START_OFFSET]); 4685 cnt = self[END_OFFSET] - self[START_OFFSET]; 4686 4687 while (n && cnt > 0) { 4688 sibling = n.nextSibling; 4689 xferNode = _traverseFullySelected(n, how); 4690 4691 if (frag) { 4692 frag.appendChild(xferNode); 4693 } 4694 4695 --cnt; 4696 n = sibling; 4697 } 4698 4699 // Nothing is partially selected, so collapse to start point 4700 if (how != CLONE) { 4701 self.collapse(TRUE); 4702 } 4703 4704 return frag; 4705 } 4706 4707 function _traverseCommonStartContainer(endAncestor, how) { 4708 var frag, n, endIdx, cnt, sibling, xferNode; 4709 4710 if (how != DELETE) { 4711 frag = createDocumentFragment(); 4712 } 4713 4714 n = _traverseRightBoundary(endAncestor, how); 4715 4716 if (frag) { 4717 frag.appendChild(n); 4718 } 4719 4720 endIdx = nodeIndex(endAncestor); 4721 cnt = endIdx - self[START_OFFSET]; 4722 4723 if (cnt <= 0) { 4724 // Collapse to just before the endAncestor, which 4725 // is partially selected. 4726 if (how != CLONE) { 4727 self.setEndBefore(endAncestor); 4728 self.collapse(FALSE); 4729 } 4730 4731 return frag; 4732 } 4733 4734 n = endAncestor.previousSibling; 4735 while (cnt > 0) { 4736 sibling = n.previousSibling; 4737 xferNode = _traverseFullySelected(n, how); 4738 4739 if (frag) { 4740 frag.insertBefore(xferNode, frag.firstChild); 4741 } 4742 4743 --cnt; 4744 n = sibling; 4745 } 4746 4747 // Collapse to just before the endAncestor, which 4748 // is partially selected. 4749 if (how != CLONE) { 4750 self.setEndBefore(endAncestor); 4751 self.collapse(FALSE); 4752 } 4753 4754 return frag; 4755 } 4756 4757 function _traverseCommonEndContainer(startAncestor, how) { 4758 var frag, startIdx, n, cnt, sibling, xferNode; 4759 4760 if (how != DELETE) { 4761 frag = createDocumentFragment(); 4762 } 4763 4764 n = _traverseLeftBoundary(startAncestor, how); 4765 if (frag) { 4766 frag.appendChild(n); 4767 } 4768 4769 startIdx = nodeIndex(startAncestor); 4770 ++startIdx; // Because we already traversed it 4771 4772 cnt = self[END_OFFSET] - startIdx; 4773 n = startAncestor.nextSibling; 4774 while (n && cnt > 0) { 4775 sibling = n.nextSibling; 4776 xferNode = _traverseFullySelected(n, how); 4777 4778 if (frag) { 4779 frag.appendChild(xferNode); 4780 } 4781 4782 --cnt; 4783 n = sibling; 4784 } 4785 4786 if (how != CLONE) { 4787 self.setStartAfter(startAncestor); 4788 self.collapse(TRUE); 4789 } 4790 4791 return frag; 4792 } 4793 4794 function _traverseCommonAncestors(startAncestor, endAncestor, how) { 4795 var n, frag, startOffset, endOffset, cnt, sibling, nextSibling; 4796 4797 if (how != DELETE) { 4798 frag = createDocumentFragment(); 4799 } 4800 4801 n = _traverseLeftBoundary(startAncestor, how); 4802 if (frag) { 4803 frag.appendChild(n); 4804 } 4805 4806 startOffset = nodeIndex(startAncestor); 4807 endOffset = nodeIndex(endAncestor); 4808 ++startOffset; 4809 4810 cnt = endOffset - startOffset; 4811 sibling = startAncestor.nextSibling; 4812 4813 while (cnt > 0) { 4814 nextSibling = sibling.nextSibling; 4815 n = _traverseFullySelected(sibling, how); 4816 4817 if (frag) { 4818 frag.appendChild(n); 4819 } 4820 4821 sibling = nextSibling; 4822 --cnt; 4823 } 4824 4825 n = _traverseRightBoundary(endAncestor, how); 4826 4827 if (frag) { 4828 frag.appendChild(n); 4829 } 4830 4831 if (how != CLONE) { 4832 self.setStartAfter(startAncestor); 4833 self.collapse(TRUE); 4834 } 4835 4836 return frag; 4837 } 4838 4839 function _traverseRightBoundary(root, how) { 4840 var next = _getSelectedNode(self[END_CONTAINER], self[END_OFFSET] - 1), parent, clonedParent; 4841 var prevSibling, clonedChild, clonedGrandParent, isFullySelected = next != self[END_CONTAINER]; 4842 4843 if (next == root) { 4844 return _traverseNode(next, isFullySelected, FALSE, how); 4845 } 4846 4847 parent = next.parentNode; 4848 clonedParent = _traverseNode(parent, FALSE, FALSE, how); 4849 4850 while (parent) { 4851 while (next) { 4852 prevSibling = next.previousSibling; 4853 clonedChild = _traverseNode(next, isFullySelected, FALSE, how); 4854 4855 if (how != DELETE) { 4856 clonedParent.insertBefore(clonedChild, clonedParent.firstChild); 4857 } 4858 4859 isFullySelected = TRUE; 4860 next = prevSibling; 4861 } 4862 4863 if (parent == root) { 4864 return clonedParent; 4865 } 4866 4867 next = parent.previousSibling; 4868 parent = parent.parentNode; 4869 4870 clonedGrandParent = _traverseNode(parent, FALSE, FALSE, how); 4871 4872 if (how != DELETE) { 4873 clonedGrandParent.appendChild(clonedParent); 4874 } 4875 4876 clonedParent = clonedGrandParent; 4877 } 4878 } 4879 4880 function _traverseLeftBoundary(root, how) { 4881 var next = _getSelectedNode(self[START_CONTAINER], self[START_OFFSET]), isFullySelected = next != self[START_CONTAINER]; 4882 var parent, clonedParent, nextSibling, clonedChild, clonedGrandParent; 4883 4884 if (next == root) { 4885 return _traverseNode(next, isFullySelected, TRUE, how); 4886 } 4887 4888 parent = next.parentNode; 4889 clonedParent = _traverseNode(parent, FALSE, TRUE, how); 4890 4891 while (parent) { 4892 while (next) { 4893 nextSibling = next.nextSibling; 4894 clonedChild = _traverseNode(next, isFullySelected, TRUE, how); 4895 4896 if (how != DELETE) { 4897 clonedParent.appendChild(clonedChild); 4898 } 4899 4900 isFullySelected = TRUE; 4901 next = nextSibling; 4902 } 4903 4904 if (parent == root) { 4905 return clonedParent; 4906 } 4907 4908 next = parent.nextSibling; 4909 parent = parent.parentNode; 4910 4911 clonedGrandParent = _traverseNode(parent, FALSE, TRUE, how); 4912 4913 if (how != DELETE) { 4914 clonedGrandParent.appendChild(clonedParent); 4915 } 4916 4917 clonedParent = clonedGrandParent; 4918 } 4919 } 4920 4921 function _traverseNode(n, isFullySelected, isLeft, how) { 4922 var txtValue, newNodeValue, oldNodeValue, offset, newNode; 4923 4924 if (isFullySelected) { 4925 return _traverseFullySelected(n, how); 4926 } 4927 4928 if (n.nodeType == 3 /* TEXT_NODE */) { 4929 txtValue = n.nodeValue; 4930 4931 if (isLeft) { 4932 offset = self[START_OFFSET]; 4933 newNodeValue = txtValue.substring(offset); 4934 oldNodeValue = txtValue.substring(0, offset); 4935 } else { 4936 offset = self[END_OFFSET]; 4937 newNodeValue = txtValue.substring(0, offset); 4938 oldNodeValue = txtValue.substring(offset); 4939 } 4940 4941 if (how != CLONE) { 4942 n.nodeValue = oldNodeValue; 4943 } 4944 4945 if (how == DELETE) { 4946 return; 4947 } 4948 4949 newNode = dom.clone(n, FALSE); 4950 newNode.nodeValue = newNodeValue; 4951 4952 return newNode; 4953 } 4954 4955 if (how == DELETE) { 4956 return; 4957 } 4958 4959 return dom.clone(n, FALSE); 4960 } 4961 4962 function _traverseFullySelected(n, how) { 4963 if (how != DELETE) { 4964 return how == CLONE ? dom.clone(n, TRUE) : n; 4965 } 4966 4967 n.parentNode.removeChild(n); 4968 } 4969 4970 function toStringIE() { 4971 return dom.create('body', null, cloneContents()).outerText; 4972 } 4973 4974 extend(self, { 4975 // Inital states 4976 startContainer: doc, 4977 startOffset: 0, 4978 endContainer: doc, 4979 endOffset: 0, 4980 collapsed: TRUE, 4981 commonAncestorContainer: doc, 4982 4983 // Range constants 4984 START_TO_START: 0, 4985 START_TO_END: 1, 4986 END_TO_END: 2, 4987 END_TO_START: 3, 4988 4989 // Public methods 4990 setStart: setStart, 4991 setEnd: setEnd, 4992 setStartBefore: setStartBefore, 4993 setStartAfter: setStartAfter, 4994 setEndBefore: setEndBefore, 4995 setEndAfter: setEndAfter, 4996 collapse: collapse, 4997 selectNode: selectNode, 4998 selectNodeContents: selectNodeContents, 4999 compareBoundaryPoints: compareBoundaryPoints, 5000 deleteContents: deleteContents, 5001 extractContents: extractContents, 5002 cloneContents: cloneContents, 5003 insertNode: insertNode, 5004 surroundContents: surroundContents, 5005 cloneRange: cloneRange, 5006 toStringIE: toStringIE 5007 }); 5008 5009 return self; 5010 } 5011 5012 // Older IE versions doesn't let you override toString by it's constructor so we have to stick it in the prototype 5013 Range.prototype.toString = function() { 5014 return this.toStringIE(); 5015 }; 5016 5017 return Range; 5018 }); 5019 5020 // Included from: js/tinymce/classes/html/Entities.js 5021 5022 /** 5023 * Entities.js 5024 * 5025 * Copyright, Moxiecode Systems AB 5026 * Released under LGPL License. 5027 * 5028 * License: http://www.tinymce.com/license 5029 * Contributing: http://www.tinymce.com/contributing 5030 */ 5031 5032 /*jshint bitwise:false */ 5033 /*eslint no-bitwise:0 */ 5034 5035 /** 5036 * Entity encoder class. 5037 * 5038 * @class tinymce.html.Entities 5039 * @static 5040 * @version 3.4 5041 */ 5042 define("tinymce/html/Entities", [ 5043 "tinymce/util/Tools" 5044 ], function(Tools) { 5045 var makeMap = Tools.makeMap; 5046 5047 var namedEntities, baseEntities, reverseEntities, 5048 attrsCharsRegExp = /[&<>\"\u0060\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g, 5049 textCharsRegExp = /[<>&\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g, 5050 rawCharsRegExp = /[<>&\"\']/g, 5051 entityRegExp = /&(#x|#)?([\w]+);/g, 5052 asciiMap = { 5053 128: "\u20AC", 130: "\u201A", 131: "\u0192", 132: "\u201E", 133: "\u2026", 134: "\u2020", 5054 135: "\u2021", 136: "\u02C6", 137: "\u2030", 138: "\u0160", 139: "\u2039", 140: "\u0152", 5055 142: "\u017D", 145: "\u2018", 146: "\u2019", 147: "\u201C", 148: "\u201D", 149: "\u2022", 5056 150: "\u2013", 151: "\u2014", 152: "\u02DC", 153: "\u2122", 154: "\u0161", 155: "\u203A", 5057 156: "\u0153", 158: "\u017E", 159: "\u0178" 5058 }; 5059 5060 // Raw entities 5061 baseEntities = { 5062 '\"': '"', // Needs to be escaped since the YUI compressor would otherwise break the code 5063 "'": ''', 5064 '<': '<', 5065 '>': '>', 5066 '&': '&', 5067 '\u0060': '`' 5068 }; 5069 5070 // Reverse lookup table for raw entities 5071 reverseEntities = { 5072 '<': '<', 5073 '>': '>', 5074 '&': '&', 5075 '"': '"', 5076 ''': "'" 5077 }; 5078 5079 // Decodes text by using the browser 5080 function nativeDecode(text) { 5081 var elm; 5082 5083 elm = document.createElement("div"); 5084 elm.innerHTML = text; 5085 5086 return elm.textContent || elm.innerText || text; 5087 } 5088 5089 // Build a two way lookup table for the entities 5090 function buildEntitiesLookup(items, radix) { 5091 var i, chr, entity, lookup = {}; 5092 5093 if (items) { 5094 items = items.split(','); 5095 radix = radix || 10; 5096 5097 // Build entities lookup table 5098 for (i = 0; i < items.length; i += 2) { 5099 chr = String.fromCharCode(parseInt(items[i], radix)); 5100 5101 // Only add non base entities 5102 if (!baseEntities[chr]) { 5103 entity = '&' + items[i + 1] + ';'; 5104 lookup[chr] = entity; 5105 lookup[entity] = chr; 5106 } 5107 } 5108 5109 return lookup; 5110 } 5111 } 5112 5113 // Unpack entities lookup where the numbers are in radix 32 to reduce the size 5114 namedEntities = buildEntitiesLookup( 5115 '50,nbsp,51,iexcl,52,cent,53,pound,54,curren,55,yen,56,brvbar,57,sect,58,uml,59,copy,' + 5116 '5a,ordf,5b,laquo,5c,not,5d,shy,5e,reg,5f,macr,5g,deg,5h,plusmn,5i,sup2,5j,sup3,5k,acute,' + 5117 '5l,micro,5m,para,5n,middot,5o,cedil,5p,sup1,5q,ordm,5r,raquo,5s,frac14,5t,frac12,5u,frac34,' + 5118 '5v,iquest,60,Agrave,61,Aacute,62,Acirc,63,Atilde,64,Auml,65,Aring,66,AElig,67,Ccedil,' + 5119 '68,Egrave,69,Eacute,6a,Ecirc,6b,Euml,6c,Igrave,6d,Iacute,6e,Icirc,6f,Iuml,6g,ETH,6h,Ntilde,' + 5120 '6i,Ograve,6j,Oacute,6k,Ocirc,6l,Otilde,6m,Ouml,6n,times,6o,Oslash,6p,Ugrave,6q,Uacute,' + 5121 '6r,Ucirc,6s,Uuml,6t,Yacute,6u,THORN,6v,szlig,70,agrave,71,aacute,72,acirc,73,atilde,74,auml,' + 5122 '75,aring,76,aelig,77,ccedil,78,egrave,79,eacute,7a,ecirc,7b,euml,7c,igrave,7d,iacute,7e,icirc,' + 5123 '7f,iuml,7g,eth,7h,ntilde,7i,ograve,7j,oacute,7k,ocirc,7l,otilde,7m,ouml,7n,divide,7o,oslash,' + 5124 '7p,ugrave,7q,uacute,7r,ucirc,7s,uuml,7t,yacute,7u,thorn,7v,yuml,ci,fnof,sh,Alpha,si,Beta,' + 5125 'sj,Gamma,sk,Delta,sl,Epsilon,sm,Zeta,sn,Eta,so,Theta,sp,Iota,sq,Kappa,sr,Lambda,ss,Mu,' + 5126 'st,Nu,su,Xi,sv,Omicron,t0,Pi,t1,Rho,t3,Sigma,t4,Tau,t5,Upsilon,t6,Phi,t7,Chi,t8,Psi,' + 5127 't9,Omega,th,alpha,ti,beta,tj,gamma,tk,delta,tl,epsilon,tm,zeta,tn,eta,to,theta,tp,iota,' + 5128 'tq,kappa,tr,lambda,ts,mu,tt,nu,tu,xi,tv,omicron,u0,pi,u1,rho,u2,sigmaf,u3,sigma,u4,tau,' + 5129 'u5,upsilon,u6,phi,u7,chi,u8,psi,u9,omega,uh,thetasym,ui,upsih,um,piv,812,bull,816,hellip,' + 5130 '81i,prime,81j,Prime,81u,oline,824,frasl,88o,weierp,88h,image,88s,real,892,trade,89l,alefsym,' + 5131 '8cg,larr,8ch,uarr,8ci,rarr,8cj,darr,8ck,harr,8dl,crarr,8eg,lArr,8eh,uArr,8ei,rArr,8ej,dArr,' + 5132 '8ek,hArr,8g0,forall,8g2,part,8g3,exist,8g5,empty,8g7,nabla,8g8,isin,8g9,notin,8gb,ni,8gf,prod,' + 5133 '8gh,sum,8gi,minus,8gn,lowast,8gq,radic,8gt,prop,8gu,infin,8h0,ang,8h7,and,8h8,or,8h9,cap,8ha,cup,' + 5134 '8hb,int,8hk,there4,8hs,sim,8i5,cong,8i8,asymp,8j0,ne,8j1,equiv,8j4,le,8j5,ge,8k2,sub,8k3,sup,8k4,' + 5135 'nsub,8k6,sube,8k7,supe,8kl,oplus,8kn,otimes,8l5,perp,8m5,sdot,8o8,lceil,8o9,rceil,8oa,lfloor,8ob,' + 5136 'rfloor,8p9,lang,8pa,rang,9ea,loz,9j0,spades,9j3,clubs,9j5,hearts,9j6,diams,ai,OElig,aj,oelig,b0,' + 5137 'Scaron,b1,scaron,bo,Yuml,m6,circ,ms,tilde,802,ensp,803,emsp,809,thinsp,80c,zwnj,80d,zwj,80e,lrm,' + 5138 '80f,rlm,80j,ndash,80k,mdash,80o,lsquo,80p,rsquo,80q,sbquo,80s,ldquo,80t,rdquo,80u,bdquo,810,dagger,' + 5139 '811,Dagger,81g,permil,81p,lsaquo,81q,rsaquo,85c,euro', 32); 5140 5141 var Entities = { 5142 /** 5143 * Encodes the specified string using raw entities. This means only the required XML base entities will be endoded. 5144 * 5145 * @method encodeRaw 5146 * @param {String} text Text to encode. 5147 * @param {Boolean} attr Optional flag to specify if the text is attribute contents. 5148 * @return {String} Entity encoded text. 5149 */ 5150 encodeRaw: function(text, attr) { 5151 return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) { 5152 return baseEntities[chr] || chr; 5153 }); 5154 }, 5155 5156 /** 5157 * Encoded the specified text with both the attributes and text entities. This function will produce larger text contents 5158 * since it doesn't know if the context is within a attribute or text node. This was added for compatibility 5159 * and is exposed as the DOMUtils.encode function. 5160 * 5161 * @method encodeAllRaw 5162 * @param {String} text Text to encode. 5163 * @return {String} Entity encoded text. 5164 */ 5165 encodeAllRaw: function(text) { 5166 return ('' + text).replace(rawCharsRegExp, function(chr) { 5167 return baseEntities[chr] || chr; 5168 }); 5169 }, 5170 5171 /** 5172 * Encodes the specified string using numeric entities. The core entities will be 5173 * encoded as named ones but all non lower ascii characters will be encoded into numeric entities. 5174 * 5175 * @method encodeNumeric 5176 * @param {String} text Text to encode. 5177 * @param {Boolean} attr Optional flag to specify if the text is attribute contents. 5178 * @return {String} Entity encoded text. 5179 */ 5180 encodeNumeric: function(text, attr) { 5181 return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) { 5182 // Multi byte sequence convert it to a single entity 5183 if (chr.length > 1) { 5184 return '' + (((chr.charCodeAt(0) - 0xD800) * 0x400) + (chr.charCodeAt(1) - 0xDC00) + 0x10000) + ';'; 5185 } 5186 5187 return baseEntities[chr] || '' + chr.charCodeAt(0) + ';'; 5188 }); 5189 }, 5190 5191 /** 5192 * Encodes the specified string using named entities. The core entities will be encoded 5193 * as named ones but all non lower ascii characters will be encoded into named entities. 5194 * 5195 * @method encodeNamed 5196 * @param {String} text Text to encode. 5197 * @param {Boolean} attr Optional flag to specify if the text is attribute contents. 5198 * @param {Object} entities Optional parameter with entities to use. 5199 * @return {String} Entity encoded text. 5200 */ 5201 encodeNamed: function(text, attr, entities) { 5202 entities = entities || namedEntities; 5203 5204 return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) { 5205 return baseEntities[chr] || entities[chr] || chr; 5206 }); 5207 }, 5208 5209 /** 5210 * Returns an encode function based on the name(s) and it's optional entities. 5211 * 5212 * @method getEncodeFunc 5213 * @param {String} name Comma separated list of encoders for example named,numeric. 5214 * @param {String} entities Optional parameter with entities to use instead of the built in set. 5215 * @return {function} Encode function to be used. 5216 */ 5217 getEncodeFunc: function(name, entities) { 5218 entities = buildEntitiesLookup(entities) || namedEntities; 5219 5220 function encodeNamedAndNumeric(text, attr) { 5221 return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) { 5222 return baseEntities[chr] || entities[chr] || '' + chr.charCodeAt(0) + ';' || chr; 5223 }); 5224 } 5225 5226 function encodeCustomNamed(text, attr) { 5227 return Entities.encodeNamed(text, attr, entities); 5228 } 5229 5230 // Replace + with , to be compatible with previous TinyMCE versions 5231 name = makeMap(name.replace(/\+/g, ',')); 5232 5233 // Named and numeric encoder 5234 if (name.named && name.numeric) { 5235 return encodeNamedAndNumeric; 5236 } 5237 5238 // Named encoder 5239 if (name.named) { 5240 // Custom names 5241 if (entities) { 5242 return encodeCustomNamed; 5243 } 5244 5245 return Entities.encodeNamed; 5246 } 5247 5248 // Numeric 5249 if (name.numeric) { 5250 return Entities.encodeNumeric; 5251 } 5252 5253 // Raw encoder 5254 return Entities.encodeRaw; 5255 }, 5256 5257 /** 5258 * Decodes the specified string, this will replace entities with raw UTF characters. 5259 * 5260 * @method decode 5261 * @param {String} text Text to entity decode. 5262 * @return {String} Entity decoded string. 5263 */ 5264 decode: function(text) { 5265 return text.replace(entityRegExp, function(all, numeric, value) { 5266 if (numeric) { 5267 value = parseInt(value, numeric.length === 2 ? 16 : 10); 5268 5269 // Support upper UTF 5270 if (value > 0xFFFF) { 5271 value -= 0x10000; 5272 5273 return String.fromCharCode(0xD800 + (value >> 10), 0xDC00 + (value & 0x3FF)); 5274 } else { 5275 return asciiMap[value] || String.fromCharCode(value); 5276 } 5277 } 5278 5279 return reverseEntities[all] || namedEntities[all] || nativeDecode(all); 5280 }); 5281 } 5282 }; 5283 5284 return Entities; 5285 }); 5286 5287 // Included from: js/tinymce/classes/Env.js 5288 5289 /** 5290 * Env.js 5291 * 5292 * Copyright, Moxiecode Systems AB 5293 * Released under LGPL License. 5294 * 5295 * License: http://www.tinymce.com/license 5296 * Contributing: http://www.tinymce.com/contributing 5297 */ 5298 5299 /** 5300 * This class contains various environment constants like browser versions etc. 5301 * Normally you don't want to sniff specific browser versions but sometimes you have 5302 * to when it's impossible to feature detect. So use this with care. 5303 * 5304 * @class tinymce.Env 5305 * @static 5306 */ 5307 define("tinymce/Env", [], function() { 5308 var nav = navigator, userAgent = nav.userAgent; 5309 var opera, webkit, ie, ie11, gecko, mac, iDevice; 5310 5311 opera = window.opera && window.opera.buildNumber; 5312 webkit = /WebKit/.test(userAgent); 5313 ie = !webkit && !opera && (/MSIE/gi).test(userAgent) && (/Explorer/gi).test(nav.appName); 5314 ie = ie && /MSIE (\w+)\./.exec(userAgent)[1]; 5315 ie11 = userAgent.indexOf('Trident/') != -1 && (userAgent.indexOf('rv:') != -1 || nav.appName.indexOf('Netscape') != -1) ? 11 : false; 5316 ie = ie || ie11; 5317 gecko = !webkit && !ie11 && /Gecko/.test(userAgent); 5318 mac = userAgent.indexOf('Mac') != -1; 5319 iDevice = /(iPad|iPhone)/.test(userAgent); 5320 5321 // Is a iPad/iPhone and not on iOS5 sniff the WebKit version since older iOS WebKit versions 5322 // says it has contentEditable support but there is no visible caret. 5323 var contentEditable = !iDevice || userAgent.match(/AppleWebKit\/(\d*)/)[1] >= 534; 5324 5325 return { 5326 /** 5327 * Constant that is true if the browser is Opera. 5328 * 5329 * @property opera 5330 * @type Boolean 5331 * @final 5332 */ 5333 opera: opera, 5334 5335 /** 5336 * Constant that is true if the browser is WebKit (Safari/Chrome). 5337 * 5338 * @property webKit 5339 * @type Boolean 5340 * @final 5341 */ 5342 webkit: webkit, 5343 5344 /** 5345 * Constant that is more than zero if the browser is IE. 5346 * 5347 * @property ie 5348 * @type Boolean 5349 * @final 5350 */ 5351 ie: ie, 5352 5353 /** 5354 * Constant that is true if the browser is Gecko. 5355 * 5356 * @property gecko 5357 * @type Boolean 5358 * @final 5359 */ 5360 gecko: gecko, 5361 5362 /** 5363 * Constant that is true if the os is Mac OS. 5364 * 5365 * @property mac 5366 * @type Boolean 5367 * @final 5368 */ 5369 mac: mac, 5370 5371 /** 5372 * Constant that is true if the os is iOS. 5373 * 5374 * @property iOS 5375 * @type Boolean 5376 * @final 5377 */ 5378 iOS: iDevice, 5379 5380 /** 5381 * Constant that is true if the browser supports editing. 5382 * 5383 * @property contentEditable 5384 * @type Boolean 5385 * @final 5386 */ 5387 contentEditable: contentEditable, 5388 5389 /** 5390 * Transparent image data url. 5391 * 5392 * @property transparentSrc 5393 * @type Boolean 5394 * @final 5395 */ 5396 transparentSrc: "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7", 5397 5398 /** 5399 * Returns true/false if the browser can or can't place the caret after a inline block like an image. 5400 * 5401 * @property noCaretAfter 5402 * @type Boolean 5403 * @final 5404 */ 5405 caretAfter: ie != 8, 5406 5407 /** 5408 * Constant that is true if the browser supports native DOM Ranges. IE 9+. 5409 * 5410 * @property range 5411 * @type Boolean 5412 */ 5413 range: window.getSelection && "Range" in window, 5414 5415 /** 5416 * Returns the IE document mode for non IE browsers this will fake IE 10. 5417 * 5418 * @property documentMode 5419 * @type Number 5420 */ 5421 documentMode: ie ? (document.documentMode || 7) : 10 5422 }; 5423 }); 5424 5425 // Included from: js/tinymce/classes/dom/StyleSheetLoader.js 5426 5427 /** 5428 * StyleSheetLoader.js 5429 * 5430 * Copyright, Moxiecode Systems AB 5431 * Released under LGPL License. 5432 * 5433 * License: http://www.tinymce.com/license 5434 * Contributing: http://www.tinymce.com/contributing 5435 */ 5436 5437 /** 5438 * This class handles loading of external stylesheets and fires events when these are loaded. 5439 * 5440 * @class tinymce.dom.StyleSheetLoader 5441 * @private 5442 */ 5443 define("tinymce/dom/StyleSheetLoader", [], function() { 5444 "use strict"; 5445 5446 return function(document, settings) { 5447 var idCount = 0, loadedStates = {}, maxLoadTime; 5448 5449 settings = settings || {}; 5450 maxLoadTime = settings.maxLoadTime || 5000; 5451 5452 function appendToHead(node) { 5453 document.getElementsByTagName('head')[0].appendChild(node); 5454 } 5455 5456 /** 5457 * Loads the specified css style sheet file and call the loadedCallback once it's finished loading. 5458 * 5459 * @method load 5460 * @param {String} url Url to be loaded. 5461 * @param {Function} loadedCallback Callback to be executed when loaded. 5462 * @param {Function} errorCallback Callback to be executed when failed loading. 5463 */ 5464 function load(url, loadedCallback, errorCallback) { 5465 var link, style, startTime, state; 5466 5467 function passed() { 5468 var callbacks = state.passed, i = callbacks.length; 5469 5470 while (i--) { 5471 callbacks[i](); 5472 } 5473 5474 state.status = 2; 5475 state.passed = []; 5476 state.failed = []; 5477 } 5478 5479 function failed() { 5480 var callbacks = state.failed, i = callbacks.length; 5481 5482 while (i--) { 5483 callbacks[i](); 5484 } 5485 5486 state.status = 3; 5487 state.passed = []; 5488 state.failed = []; 5489 } 5490 5491 // Sniffs for older WebKit versions that have the link.onload but a broken one 5492 function isOldWebKit() { 5493 var webKitChunks = navigator.userAgent.match(/WebKit\/(\d*)/); 5494 return !!(webKitChunks && webKitChunks[1] < 536); 5495 } 5496 5497 // Calls the waitCallback until the test returns true or the timeout occurs 5498 function wait(testCallback, waitCallback) { 5499 if (!testCallback()) { 5500 // Wait for timeout 5501 if ((new Date().getTime()) - startTime < maxLoadTime) { 5502 window.setTimeout(waitCallback, 0); 5503 } else { 5504 failed(); 5505 } 5506 } 5507 } 5508 5509 // Workaround for WebKit that doesn't properly support the onload event for link elements 5510 // Or WebKit that fires the onload event before the StyleSheet is added to the document 5511 function waitForWebKitLinkLoaded() { 5512 wait(function() { 5513 var styleSheets = document.styleSheets, styleSheet, i = styleSheets.length, owner; 5514 5515 while (i--) { 5516 styleSheet = styleSheets[i]; 5517 owner = styleSheet.ownerNode ? styleSheet.ownerNode : styleSheet.owningElement; 5518 if (owner && owner.id === link.id) { 5519 passed(); 5520 return true; 5521 } 5522 } 5523 }, waitForWebKitLinkLoaded); 5524 } 5525 5526 // Workaround for older Geckos that doesn't have any onload event for StyleSheets 5527 function waitForGeckoLinkLoaded() { 5528 wait(function() { 5529 try { 5530 // Accessing the cssRules will throw an exception until the CSS file is loaded 5531 var cssRules = style.sheet.cssRules; 5532 passed(); 5533 return !!cssRules; 5534 } catch (ex) { 5535 // Ignore 5536 } 5537 }, waitForGeckoLinkLoaded); 5538 } 5539 5540 if (!loadedStates[url]) { 5541 state = { 5542 passed: [], 5543 failed: [] 5544 }; 5545 5546 loadedStates[url] = state; 5547 } else { 5548 state = loadedStates[url]; 5549 } 5550 5551 if (loadedCallback) { 5552 state.passed.push(loadedCallback); 5553 } 5554 5555 if (errorCallback) { 5556 state.failed.push(errorCallback); 5557 } 5558 5559 // Is loading wait for it to pass 5560 if (state.status == 1) { 5561 return; 5562 } 5563 5564 // Has finished loading and was success 5565 if (state.status == 2) { 5566 passed(); 5567 return; 5568 } 5569 5570 // Has finished loading and was a failure 5571 if (state.status == 3) { 5572 failed(); 5573 return; 5574 } 5575 5576 // Start loading 5577 state.status = 1; 5578 link = document.createElement('link'); 5579 link.rel = 'stylesheet'; 5580 link.type = 'text/css'; 5581 link.id = 'u' + (idCount++); 5582 link.async = false; 5583 link.defer = false; 5584 startTime = new Date().getTime(); 5585 5586 // Feature detect onload on link element and sniff older webkits since it has an broken onload event 5587 if ("onload" in link && !isOldWebKit()) { 5588 link.onload = waitForWebKitLinkLoaded; 5589 link.onerror = failed; 5590 } else { 5591 // Sniff for old Firefox that doesn't support the onload event on link elements 5592 // TODO: Remove this in the future when everyone uses modern browsers 5593 if (navigator.userAgent.indexOf("Firefox") > 0) { 5594 style = document.createElement('style'); 5595 style.textContent = '@import "' + url + '"'; 5596 waitForGeckoLinkLoaded(); 5597 appendToHead(style); 5598 return; 5599 } else { 5600 // Use the id owner on older webkits 5601 waitForWebKitLinkLoaded(); 5602 } 5603 } 5604 5605 appendToHead(link); 5606 link.href = url; 5607 } 5608 5609 this.load = load; 5610 }; 5611 }); 5612 5613 // Included from: js/tinymce/classes/dom/DOMUtils.js 5614 5615 /** 5616 * DOMUtils.js 5617 * 5618 * Copyright, Moxiecode Systems AB 5619 * Released under LGPL License. 5620 * 5621 * License: http://www.tinymce.com/license 5622 * Contributing: http://www.tinymce.com/contributing 5623 */ 5624 5625 /** 5626 * Utility class for various DOM manipulation and retrieval functions. 5627 * 5628 * @class tinymce.dom.DOMUtils 5629 * @example 5630 * // Add a class to an element by id in the page 5631 * tinymce.DOM.addClass('someid', 'someclass'); 5632 * 5633 * // Add a class to an element by id inside the editor 5634 * tinymce.activeEditor.dom.addClass('someid', 'someclass'); 5635 */ 5636 define("tinymce/dom/DOMUtils", [ 5637 "tinymce/dom/Sizzle", 5638 "tinymce/html/Styles", 5639 "tinymce/dom/EventUtils", 5640 "tinymce/dom/TreeWalker", 5641 "tinymce/dom/Range", 5642 "tinymce/html/Entities", 5643 "tinymce/Env", 5644 "tinymce/util/Tools", 5645 "tinymce/dom/StyleSheetLoader" 5646 ], function(Sizzle, Styles, EventUtils, TreeWalker, Range, Entities, Env, Tools, StyleSheetLoader) { 5647 // Shorten names 5648 var each = Tools.each, is = Tools.is, grep = Tools.grep, trim = Tools.trim, extend = Tools.extend; 5649 var isWebKit = Env.webkit, isIE = Env.ie; 5650 var simpleSelectorRe = /^([a-z0-9],?)+$/i; 5651 var whiteSpaceRegExp = /^[ \t\r\n]*$/; 5652 var numericCssMap = Tools.makeMap('fillOpacity fontWeight lineHeight opacity orphans widows zIndex zoom', ' '); 5653 5654 /** 5655 * Constructs a new DOMUtils instance. Consult the Wiki for more details on settings etc for this class. 5656 * 5657 * @constructor 5658 * @method DOMUtils 5659 * @param {Document} d Document reference to bind the utility class to. 5660 * @param {settings} s Optional settings collection. 5661 */ 5662 function DOMUtils(doc, settings) { 5663 var self = this, blockElementsMap; 5664 5665 self.doc = doc; 5666 self.win = window; 5667 self.files = {}; 5668 self.counter = 0; 5669 self.stdMode = !isIE || doc.documentMode >= 8; 5670 self.boxModel = !isIE || doc.compatMode == "CSS1Compat" || self.stdMode; 5671 self.hasOuterHTML = "outerHTML" in doc.createElement("a"); 5672 self.styleSheetLoader = new StyleSheetLoader(doc); 5673 this.boundEvents = []; 5674 5675 self.settings = settings = extend({ 5676 keep_values: false, 5677 hex_colors: 1 5678 }, settings); 5679 5680 self.schema = settings.schema; 5681 self.styles = new Styles({ 5682 url_converter: settings.url_converter, 5683 url_converter_scope: settings.url_converter_scope 5684 }, settings.schema); 5685 5686 self.fixDoc(doc); 5687 self.events = settings.ownEvents ? new EventUtils(settings.proxy) : EventUtils.Event; 5688 blockElementsMap = settings.schema ? settings.schema.getBlockElements() : {}; 5689 5690 /** 5691 * Returns true/false if the specified element is a block element or not. 5692 * 5693 * @method isBlock 5694 * @param {Node/String} node Element/Node to check. 5695 * @return {Boolean} True/False state if the node is a block element or not. 5696 */ 5697 self.isBlock = function(node) { 5698 // Fix for #5446 5699 if (!node) { 5700 return false; 5701 } 5702 5703 // This function is called in module pattern style since it might be executed with the wrong this scope 5704 var type = node.nodeType; 5705 5706 // If it's a node then check the type and use the nodeName 5707 if (type) { 5708 return !!(type === 1 && blockElementsMap[node.nodeName]); 5709 } 5710 5711 return !!blockElementsMap[node]; 5712 }; 5713 } 5714 5715 DOMUtils.prototype = { 5716 root: null, 5717 props: { 5718 "for": "htmlFor", 5719 "class": "className", 5720 className: "className", 5721 checked: "checked", 5722 disabled: "disabled", 5723 maxlength: "maxLength", 5724 readonly: "readOnly", 5725 selected: "selected", 5726 value: "value", 5727 id: "id", 5728 name: "name", 5729 type: "type" 5730 }, 5731 5732 fixDoc: function(doc) { 5733 var settings = this.settings, name; 5734 5735 if (isIE && settings.schema) { 5736 // Add missing HTML 4/5 elements to IE 5737 ('abbr article aside audio canvas ' + 5738 'details figcaption figure footer ' + 5739 'header hgroup mark menu meter nav ' + 5740 'output progress section summary ' + 5741 'time video').replace(/\w+/g, function(name) { 5742 doc.createElement(name); 5743 }); 5744 5745 // Create all custom elements 5746 for (name in settings.schema.getCustomElements()) { 5747 doc.createElement(name); 5748 } 5749 } 5750 }, 5751 5752 clone: function(node, deep) { 5753 var self = this, clone, doc; 5754 5755 // TODO: Add feature detection here in the future 5756 if (!isIE || node.nodeType !== 1 || deep) { 5757 return node.cloneNode(deep); 5758 } 5759 5760 doc = self.doc; 5761 5762 // Make a HTML5 safe shallow copy 5763 if (!deep) { 5764 clone = doc.createElement(node.nodeName); 5765 5766 // Copy attribs 5767 each(self.getAttribs(node), function(attr) { 5768 self.setAttrib(clone, attr.nodeName, self.getAttrib(node, attr.nodeName)); 5769 }); 5770 5771 return clone; 5772 } 5773 /* 5774 // Setup HTML5 patched document fragment 5775 if (!self.frag) { 5776 self.frag = doc.createDocumentFragment(); 5777 self.fixDoc(self.frag); 5778 } 5779 5780 // Make a deep copy by adding it to the document fragment then removing it this removed the :section 5781 clone = doc.createElement('div'); 5782 self.frag.appendChild(clone); 5783 clone.innerHTML = node.outerHTML; 5784 self.frag.removeChild(clone); 5785 */ 5786 return clone.firstChild; 5787 }, 5788 5789 /** 5790 * Returns the root node of the document. This is normally the body but might be a DIV. Parents like getParent will not 5791 * go above the point of this root node. 5792 * 5793 * @method getRoot 5794 * @return {Element} Root element for the utility class. 5795 */ 5796 getRoot: function() { 5797 var self = this; 5798 5799 return self.get(self.settings.root_element) || self.doc.body; 5800 }, 5801 5802 /** 5803 * Returns the viewport of the window. 5804 * 5805 * @method getViewPort 5806 * @param {Window} win Optional window to get viewport of. 5807 * @return {Object} Viewport object with fields x, y, w and h. 5808 */ 5809 getViewPort: function(win) { 5810 var doc, rootElm; 5811 5812 win = !win ? this.win : win; 5813 doc = win.document; 5814 rootElm = this.boxModel ? doc.documentElement : doc.body; 5815 5816 // Returns viewport size excluding scrollbars 5817 return { 5818 x: win.pageXOffset || rootElm.scrollLeft, 5819 y: win.pageYOffset || rootElm.scrollTop, 5820 w: win.innerWidth || rootElm.clientWidth, 5821 h: win.innerHeight || rootElm.clientHeight 5822 }; 5823 }, 5824 5825 /** 5826 * Returns the rectangle for a specific element. 5827 * 5828 * @method getRect 5829 * @param {Element/String} elm Element object or element ID to get rectangle from. 5830 * @return {object} Rectangle for specified element object with x, y, w, h fields. 5831 */ 5832 getRect: function(elm) { 5833 var self = this, pos, size; 5834 5835 elm = self.get(elm); 5836 pos = self.getPos(elm); 5837 size = self.getSize(elm); 5838 5839 return { 5840 x: pos.x, y: pos.y, 5841 w: size.w, h: size.h 5842 }; 5843 }, 5844 5845 /** 5846 * Returns the size dimensions of the specified element. 5847 * 5848 * @method getSize 5849 * @param {Element/String} elm Element object or element ID to get rectangle from. 5850 * @return {object} Rectangle for specified element object with w, h fields. 5851 */ 5852 getSize: function(elm) { 5853 var self = this, w, h; 5854 5855 elm = self.get(elm); 5856 w = self.getStyle(elm, 'width'); 5857 h = self.getStyle(elm, 'height'); 5858 5859 // Non pixel value, then force offset/clientWidth 5860 if (w.indexOf('px') === -1) { 5861 w = 0; 5862 } 5863 5864 // Non pixel value, then force offset/clientWidth 5865 if (h.indexOf('px') === -1) { 5866 h = 0; 5867 } 5868 5869 return { 5870 w: parseInt(w, 10) || elm.offsetWidth || elm.clientWidth, 5871 h: parseInt(h, 10) || elm.offsetHeight || elm.clientHeight 5872 }; 5873 }, 5874 5875 /** 5876 * Returns a node by the specified selector function. This function will 5877 * loop through all parent nodes and call the specified function for each node. 5878 * If the function then returns true indicating that it has found what it was looking for, the loop execution will then end 5879 * and the node it found will be returned. 5880 * 5881 * @method getParent 5882 * @param {Node/String} node DOM node to search parents on or ID string. 5883 * @param {function} selector Selection function or CSS selector to execute on each node. 5884 * @param {Node} root Optional root element, never go below this point. 5885 * @return {Node} DOM Node or null if it wasn't found. 5886 */ 5887 getParent: function(node, selector, root) { 5888 return this.getParents(node, selector, root, false); 5889 }, 5890 5891 /** 5892 * Returns a node list of all parents matching the specified selector function or pattern. 5893 * If the function then returns true indicating that it has found what it was looking for and that node will be collected. 5894 * 5895 * @method getParents 5896 * @param {Node/String} node DOM node to search parents on or ID string. 5897 * @param {function} selector Selection function to execute on each node or CSS pattern. 5898 * @param {Node} root Optional root element, never go below this point. 5899 * @return {Array} Array of nodes or null if it wasn't found. 5900 */ 5901 getParents: function(node, selector, root, collect) { 5902 var self = this, selectorVal, result = []; 5903 5904 node = self.get(node); 5905 collect = collect === undefined; 5906 5907 // Default root on inline mode 5908 root = root || (self.getRoot().nodeName != 'BODY' ? self.getRoot().parentNode : null); 5909 5910 // Wrap node name as func 5911 if (is(selector, 'string')) { 5912 selectorVal = selector; 5913 5914 if (selector === '*') { 5915 selector = function(node) {return node.nodeType == 1;}; 5916 } else { 5917 selector = function(node) { 5918 return self.is(node, selectorVal); 5919 }; 5920 } 5921 } 5922 5923 while (node) { 5924 if (node == root || !node.nodeType || node.nodeType === 9) { 5925 break; 5926 } 5927 5928 if (!selector || selector(node)) { 5929 if (collect) { 5930 result.push(node); 5931 } else { 5932 return node; 5933 } 5934 } 5935 5936 node = node.parentNode; 5937 } 5938 5939 return collect ? result : null; 5940 }, 5941 5942 /** 5943 * Returns the specified element by ID or the input element if it isn't a string. 5944 * 5945 * @method get 5946 * @param {String/Element} n Element id to look for or element to just pass though. 5947 * @return {Element} Element matching the specified id or null if it wasn't found. 5948 */ 5949 get: function(elm) { 5950 var name; 5951 5952 if (elm && this.doc && typeof(elm) == 'string') { 5953 name = elm; 5954 elm = this.doc.getElementById(elm); 5955 5956 // IE and Opera returns meta elements when they match the specified input ID, but getElementsByName seems to do the trick 5957 if (elm && elm.id !== name) { 5958 return this.doc.getElementsByName(name)[1]; 5959 } 5960 } 5961 5962 return elm; 5963 }, 5964 5965 /** 5966 * Returns the next node that matches selector or function 5967 * 5968 * @method getNext 5969 * @param {Node} node Node to find siblings from. 5970 * @param {String/function} selector Selector CSS expression or function. 5971 * @return {Node} Next node item matching the selector or null if it wasn't found. 5972 */ 5973 getNext: function(node, selector) { 5974 return this._findSib(node, selector, 'nextSibling'); 5975 }, 5976 5977 /** 5978 * Returns the previous node that matches selector or function 5979 * 5980 * @method getPrev 5981 * @param {Node} node Node to find siblings from. 5982 * @param {String/function} selector Selector CSS expression or function. 5983 * @return {Node} Previous node item matching the selector or null if it wasn't found. 5984 */ 5985 getPrev: function(node, selector) { 5986 return this._findSib(node, selector, 'previousSibling'); 5987 }, 5988 5989 // #ifndef jquery 5990 5991 /** 5992 * Selects specific elements by a CSS level 3 pattern. For example "div#a1 p.test". 5993 * This function is optimized for the most common patterns needed in TinyMCE but it also performs well enough 5994 * on more complex patterns. 5995 * 5996 * @method select 5997 * @param {String} selector CSS level 3 pattern to select/find elements by. 5998 * @param {Object} scope Optional root element/scope element to search in. 5999 * @return {Array} Array with all matched elements. 6000 * @example 6001 * // Adds a class to all paragraphs in the currently active editor 6002 * tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('p'), 'someclass'); 6003 * 6004 * // Adds a class to all spans that have the test class in the currently active editor 6005 * tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('span.test'), 'someclass') 6006 */ 6007 select: function(selector, scope) { 6008 var self = this; 6009 6010 //Sizzle.selectors.cacheLength = 0; 6011 return Sizzle(selector, self.get(scope) || self.get(self.settings.root_element) || self.doc, []); 6012 }, 6013 6014 /** 6015 * Returns true/false if the specified element matches the specified css pattern. 6016 * 6017 * @method is 6018 * @param {Node/NodeList} elm DOM node to match or an array of nodes to match. 6019 * @param {String} selector CSS pattern to match the element against. 6020 */ 6021 is: function(elm, selector) { 6022 var i; 6023 6024 // If it isn't an array then try to do some simple selectors instead of Sizzle for to boost performance 6025 if (elm.length === undefined) { 6026 // Simple all selector 6027 if (selector === '*') { 6028 return elm.nodeType == 1; 6029 } 6030 6031 // Simple selector just elements 6032 if (simpleSelectorRe.test(selector)) { 6033 selector = selector.toLowerCase().split(/,/); 6034 elm = elm.nodeName.toLowerCase(); 6035 6036 for (i = selector.length - 1; i >= 0; i--) { 6037 if (selector[i] == elm) { 6038 return true; 6039 } 6040 } 6041 6042 return false; 6043 } 6044 } 6045 6046 // Is non element 6047 if (elm.nodeType && elm.nodeType != 1) { 6048 return false; 6049 } 6050 6051 var elms = elm.nodeType ? [elm] : elm; 6052 return Sizzle(selector, elms[0].ownerDocument || elms[0], null, elms).length > 0; 6053 }, 6054 6055 // #endif 6056 6057 /** 6058 * Adds the specified element to another element or elements. 6059 * 6060 * @method add 6061 * @param {String/Element/Array} parentElm Element id string, DOM node element or array of ids or elements to add to. 6062 * @param {String/Element} name Name of new element to add or existing element to add. 6063 * @param {Object} attrs Optional object collection with arguments to add to the new element(s). 6064 * @param {String} html Optional inner HTML contents to add for each element. 6065 * @return {Element/Array} Element that got created, or an array of created elements if multiple input elements 6066 * were passed in. 6067 * @example 6068 * // Adds a new paragraph to the end of the active editor 6069 * tinymce.activeEditor.dom.add(tinymce.activeEditor.getBody(), 'p', {title: 'my title'}, 'Some content'); 6070 */ 6071 add: function(parentElm, name, attrs, html, create) { 6072 var self = this; 6073 6074 return this.run(parentElm, function(parentElm) { 6075 var newElm; 6076 6077 newElm = is(name, 'string') ? self.doc.createElement(name) : name; 6078 self.setAttribs(newElm, attrs); 6079 6080 if (html) { 6081 if (html.nodeType) { 6082 newElm.appendChild(html); 6083 } else { 6084 self.setHTML(newElm, html); 6085 } 6086 } 6087 6088 return !create ? parentElm.appendChild(newElm) : newElm; 6089 }); 6090 }, 6091 6092 /** 6093 * Creates a new element. 6094 * 6095 * @method create 6096 * @param {String} name Name of new element. 6097 * @param {Object} attrs Optional object name/value collection with element attributes. 6098 * @param {String} html Optional HTML string to set as inner HTML of the element. 6099 * @return {Element} HTML DOM node element that got created. 6100 * @example 6101 * // Adds an element where the caret/selection is in the active editor 6102 * var el = tinymce.activeEditor.dom.create('div', {id: 'test', 'class': 'myclass'}, 'some content'); 6103 * tinymce.activeEditor.selection.setNode(el); 6104 */ 6105 create: function(name, attrs, html) { 6106 return this.add(this.doc.createElement(name), name, attrs, html, 1); 6107 }, 6108 6109 /** 6110 * Creates HTML string for element. The element will be closed unless an empty inner HTML string is passed in. 6111 * 6112 * @method createHTML 6113 * @param {String} name Name of new element. 6114 * @param {Object} attrs Optional object name/value collection with element attributes. 6115 * @param {String} html Optional HTML string to set as inner HTML of the element. 6116 * @return {String} String with new HTML element, for example: <a href="#">test</a>. 6117 * @example 6118 * // Creates a html chunk and inserts it at the current selection/caret location 6119 * tinymce.activeEditor.selection.setContent(tinymce.activeEditor.dom.createHTML('a', {href: 'test.html'}, 'some line')); 6120 */ 6121 createHTML: function(name, attrs, html) { 6122 var outHtml = '', key; 6123 6124 outHtml += '<' + name; 6125 6126 for (key in attrs) { 6127 if (attrs.hasOwnProperty(key) && attrs[key] !== null) { 6128 outHtml += ' ' + key + '="' + this.encode(attrs[key]) + '"'; 6129 } 6130 } 6131 6132 // A call to tinymce.is doesn't work for some odd reason on IE9 possible bug inside their JS runtime 6133 if (typeof(html) != "undefined") { 6134 return outHtml + '>' + html + '</' + name + '>'; 6135 } 6136 6137 return outHtml + ' />'; 6138 }, 6139 6140 /** 6141 * Creates a document fragment out of the specified HTML string. 6142 * 6143 * @method createFragment 6144 * @param {String} html Html string to create fragment from. 6145 * @return {DocumentFragment} Document fragment node. 6146 */ 6147 createFragment: function(html) { 6148 var frag, node, doc = this.doc, container; 6149 6150 container = doc.createElement("div"); 6151 frag = doc.createDocumentFragment(); 6152 6153 if (html) { 6154 container.innerHTML = html; 6155 } 6156 6157 while ((node = container.firstChild)) { 6158 frag.appendChild(node); 6159 } 6160 6161 return frag; 6162 }, 6163 6164 /** 6165 * Removes/deletes the specified element(s) from the DOM. 6166 * 6167 * @method remove 6168 * @param {String/Element/Array} node ID of element or DOM element object or array containing multiple elements/ids. 6169 * @param {Boolean} keep_children Optional state to keep children or not. If set to true all children will be 6170 * placed at the location of the removed element. 6171 * @return {Element/Array} HTML DOM element that got removed, or an array of removed elements if multiple input elements 6172 * were passed in. 6173 * @example 6174 * // Removes all paragraphs in the active editor 6175 * tinymce.activeEditor.dom.remove(tinymce.activeEditor.dom.select('p')); 6176 * 6177 * // Removes an element by id in the document 6178 * tinymce.DOM.remove('mydiv'); 6179 */ 6180 remove: function(node, keep_children) { 6181 return this.run(node, function(node) { 6182 var child, parent = node.parentNode; 6183 6184 if (!parent) { 6185 return null; 6186 } 6187 6188 if (keep_children) { 6189 while ((child = node.firstChild)) { 6190 // IE 8 will crash if you don't remove completely empty text nodes 6191 if (!isIE || child.nodeType !== 3 || child.nodeValue) { 6192 parent.insertBefore(child, node); 6193 } else { 6194 node.removeChild(child); 6195 } 6196 } 6197 } 6198 6199 return parent.removeChild(node); 6200 }); 6201 }, 6202 6203 /** 6204 * Sets the CSS style value on a HTML element. The name can be a camelcase string 6205 * or the CSS style name like background-color. 6206 * 6207 * @method setStyle 6208 * @param {String/Element/Array} n HTML element/Element ID or Array of elements/ids to set CSS style value on. 6209 * @param {String} na Name of the style value to set. 6210 * @param {String} v Value to set on the style. 6211 * @example 6212 * // Sets a style value on all paragraphs in the currently active editor 6213 * tinymce.activeEditor.dom.setStyle(tinymce.activeEditor.dom.select('p'), 'background-color', 'red'); 6214 * 6215 * // Sets a style value to an element by id in the current document 6216 * tinymce.DOM.setStyle('mydiv', 'background-color', 'red'); 6217 */ 6218 setStyle: function(elm, name, value) { 6219 return this.run(elm, function(elm) { 6220 var self = this, style, key; 6221 6222 if (name) { 6223 if (typeof(name) === 'string') { 6224 style = elm.style; 6225 6226 // Camelcase it, if needed 6227 name = name.replace(/-(\D)/g, function(a, b) { 6228 return b.toUpperCase(); 6229 }); 6230 6231 // Default px suffix on these 6232 if (typeof(value) === 'number' && !numericCssMap[name]) { 6233 value += 'px'; 6234 } 6235 6236 // IE specific opacity 6237 if (name === "opacity" && elm.runtimeStyle && typeof(elm.runtimeStyle.opacity) === "undefined") { 6238 style.filter = value === '' ? '' : "alpha(opacity=" + (value * 100) + ")"; 6239 } 6240 6241 if (name == "float") { 6242 // Old IE vs modern browsers 6243 name = "cssFloat" in elm.style ? "cssFloat" : "styleFloat"; 6244 } 6245 6246 try { 6247 style[name] = value; 6248 } catch (ex) { 6249 // Ignore IE errors 6250 } 6251 6252 // Force update of the style data 6253 if (self.settings.update_styles) { 6254 elm.removeAttribute('data-mce-style'); 6255 } 6256 } else { 6257 for (key in name) { 6258 self.setStyle(elm, key, name[key]); 6259 } 6260 } 6261 } 6262 }); 6263 }, 6264 6265 /** 6266 * Returns the current style or runtime/computed value of an element. 6267 * 6268 * @method getStyle 6269 * @param {String/Element} elm HTML element or element id string to get style from. 6270 * @param {String} name Style name to return. 6271 * @param {Boolean} computed Computed style. 6272 * @return {String} Current style or computed style value of an element. 6273 */ 6274 getStyle: function(elm, name, computed) { 6275 elm = this.get(elm); 6276 6277 if (!elm) { 6278 return; 6279 } 6280 6281 // W3C 6282 if (this.doc.defaultView && computed) { 6283 // Remove camelcase 6284 name = name.replace(/[A-Z]/g, function(a){ 6285 return '-' + a; 6286 }); 6287 6288 try { 6289 return this.doc.defaultView.getComputedStyle(elm, null).getPropertyValue(name); 6290 } catch (ex) { 6291 // Old safari might fail 6292 return null; 6293 } 6294 } 6295 6296 // Camelcase it, if needed 6297 name = name.replace(/-(\D)/g, function(a, b) { 6298 return b.toUpperCase(); 6299 }); 6300 6301 if (name == 'float') { 6302 name = isIE ? 'styleFloat' : 'cssFloat'; 6303 } 6304 6305 // IE & Opera 6306 if (elm.currentStyle && computed) { 6307 return elm.currentStyle[name]; 6308 } 6309 6310 return elm.style ? elm.style[name] : undefined; 6311 }, 6312 6313 /** 6314 * Sets multiple styles on the specified element(s). 6315 * 6316 * @method setStyles 6317 * @param {Element/String/Array} e DOM element, element id string or array of elements/ids to set styles on. 6318 * @param {Object} o Name/Value collection of style items to add to the element(s). 6319 * @example 6320 * // Sets styles on all paragraphs in the currently active editor 6321 * tinymce.activeEditor.dom.setStyles(tinymce.activeEditor.dom.select('p'), {'background-color': 'red', 'color': 'green'}); 6322 * 6323 * // Sets styles to an element by id in the current document 6324 * tinymce.DOM.setStyles('mydiv', {'background-color': 'red', 'color': 'green'}); 6325 */ 6326 setStyles: function(elm, styles) { 6327 this.setStyle(elm, styles); 6328 }, 6329 6330 css: function(elm, name, value) { 6331 this.setStyle(elm, name, value); 6332 }, 6333 6334 /** 6335 * Removes all attributes from an element or elements. 6336 * 6337 * @method removeAllAttribs 6338 * @param {Element/String/Array} e DOM element, element id string or array of elements/ids to remove attributes from. 6339 */ 6340 removeAllAttribs: function(e) { 6341 return this.run(e, function(e) { 6342 var i, attrs = e.attributes; 6343 for (i = attrs.length - 1; i >= 0; i--) { 6344 e.removeAttributeNode(attrs.item(i)); 6345 } 6346 }); 6347 }, 6348 6349 /** 6350 * Sets the specified attribute of an element or elements. 6351 * 6352 * @method setAttrib 6353 * @param {Element/String/Array} e DOM element, element id string or array of elements/ids to set attribute on. 6354 * @param {String} n Name of attribute to set. 6355 * @param {String} v Value to set on the attribute - if this value is falsy like null, 0 or '' it will remove the attribute instead. 6356 * @example 6357 * // Sets class attribute on all paragraphs in the active editor 6358 * tinymce.activeEditor.dom.setAttrib(tinymce.activeEditor.dom.select('p'), 'class', 'myclass'); 6359 * 6360 * // Sets class attribute on a specific element in the current page 6361 * tinymce.dom.setAttrib('mydiv', 'class', 'myclass'); 6362 */ 6363 setAttrib: function(e, n, v) { 6364 var self = this; 6365 6366 // What's the point 6367 if (!e || !n) { 6368 return; 6369 } 6370 6371 return this.run(e, function(e) { 6372 var s = self.settings; 6373 var originalValue = e.getAttribute(n); 6374 if (v !== null) { 6375 switch (n) { 6376 case "style": 6377 if (!is(v, 'string')) { 6378 each(v, function(v, n) { 6379 self.setStyle(e, n, v); 6380 }); 6381 6382 return; 6383 } 6384 6385 // No mce_style for elements with these since they might get resized by the user 6386 if (s.keep_values) { 6387 if (v) { 6388 e.setAttribute('data-mce-style', v, 2); 6389 } else { 6390 e.removeAttribute('data-mce-style', 2); 6391 } 6392 } 6393 6394 e.style.cssText = v; 6395 break; 6396 6397 case "class": 6398 e.className = v || ''; // Fix IE null bug 6399 break; 6400 6401 case "src": 6402 case "href": 6403 if (s.keep_values) { 6404 if (s.url_converter) { 6405 v = s.url_converter.call(s.url_converter_scope || self, v, n, e); 6406 } 6407 6408 self.setAttrib(e, 'data-mce-' + n, v, 2); 6409 } 6410 6411 break; 6412 6413 case "shape": 6414 e.setAttribute('data-mce-style', v); 6415 break; 6416 } 6417 } 6418 if (is(v) && v !== null && v.length !== 0) { 6419 e.setAttribute(n, '' + v, 2); 6420 } else { 6421 e.removeAttribute(n, 2); 6422 } 6423 6424 // fire onChangeAttrib event for attributes that have changed 6425 if (originalValue != v && s.onSetAttrib) { 6426 s.onSetAttrib({attrElm: e, attrName: n, attrValue: v}); 6427 } 6428 }); 6429 }, 6430 6431 /** 6432 * Sets two or more specified attributes of an element or elements. 6433 * 6434 * @method setAttribs 6435 * @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set attributes on. 6436 * @param {Object} attrs Name/Value collection of attribute items to add to the element(s). 6437 * @example 6438 * // Sets class and title attributes on all paragraphs in the active editor 6439 * tinymce.activeEditor.dom.setAttribs(tinymce.activeEditor.dom.select('p'), {'class': 'myclass', title: 'some title'}); 6440 * 6441 * // Sets class and title attributes on a specific element in the current page 6442 * tinymce.DOM.setAttribs('mydiv', {'class': 'myclass', title: 'some title'}); 6443 */ 6444 setAttribs: function(elm, attrs) { 6445 var self = this; 6446 6447 return this.run(elm, function(elm) { 6448 each(attrs, function(value, name) { 6449 self.setAttrib(elm, name, value); 6450 }); 6451 }); 6452 }, 6453 6454 /** 6455 * Returns the specified attribute by name. 6456 * 6457 * @method getAttrib 6458 * @param {String/Element} elm Element string id or DOM element to get attribute from. 6459 * @param {String} name Name of attribute to get. 6460 * @param {String} defaultVal Optional default value to return if the attribute didn't exist. 6461 * @return {String} Attribute value string, default value or null if the attribute wasn't found. 6462 */ 6463 getAttrib: function(elm, name, defaultVal) { 6464 var value, self = this, undef; 6465 6466 elm = self.get(elm); 6467 6468 if (!elm || elm.nodeType !== 1) { 6469 return defaultVal === undef ? false : defaultVal; 6470 } 6471 6472 if (!is(defaultVal)) { 6473 defaultVal = ''; 6474 } 6475 6476 // Try the mce variant for these 6477 if (/^(src|href|style|coords|shape)$/.test(name)) { 6478 value = elm.getAttribute("data-mce-" + name); 6479 6480 if (value) { 6481 return value; 6482 } 6483 } 6484 6485 if (isIE && self.props[name]) { 6486 value = elm[self.props[name]]; 6487 value = value && value.nodeValue ? value.nodeValue : value; 6488 } 6489 6490 if (!value) { 6491 value = elm.getAttribute(name, 2); 6492 } 6493 6494 // Check boolean attribs 6495 if (/^(checked|compact|declare|defer|disabled|ismap|multiple|nohref|noshade|nowrap|readonly|selected)$/.test(name)) { 6496 if (elm[self.props[name]] === true && value === '') { 6497 return name; 6498 } 6499 6500 return value ? name : ''; 6501 } 6502 6503 // Inner input elements will override attributes on form elements 6504 if (elm.nodeName === "FORM" && elm.getAttributeNode(name)) { 6505 return elm.getAttributeNode(name).nodeValue; 6506 } 6507 6508 if (name === 'style') { 6509 value = value || elm.style.cssText; 6510 6511 if (value) { 6512 value = self.serializeStyle(self.parseStyle(value), elm.nodeName); 6513 6514 if (self.settings.keep_values) { 6515 elm.setAttribute('data-mce-style', value); 6516 } 6517 } 6518 } 6519 6520 // Remove Apple and WebKit stuff 6521 if (isWebKit && name === "class" && value) { 6522 value = value.replace(/(apple|webkit)\-[a-z\-]+/gi, ''); 6523 } 6524 6525 // Handle IE issues 6526 if (isIE) { 6527 switch (name) { 6528 case 'rowspan': 6529 case 'colspan': 6530 // IE returns 1 as default value 6531 if (value === 1) { 6532 value = ''; 6533 } 6534 6535 break; 6536 6537 case 'size': 6538 // IE returns +0 as default value for size 6539 if (value === '+0' || value === 20 || value === 0) { 6540 value = ''; 6541 } 6542 6543 break; 6544 6545 case 'width': 6546 case 'height': 6547 case 'vspace': 6548 case 'checked': 6549 case 'disabled': 6550 case 'readonly': 6551 if (value === 0) { 6552 value = ''; 6553 } 6554 6555 break; 6556 6557 case 'hspace': 6558 // IE returns -1 as default value 6559 if (value === -1) { 6560 value = ''; 6561 } 6562 6563 break; 6564 6565 case 'maxlength': 6566 case 'tabindex': 6567 // IE returns default value 6568 if (value === 32768 || value === 2147483647 || value === '32768') { 6569 value = ''; 6570 } 6571 6572 break; 6573 6574 case 'multiple': 6575 case 'compact': 6576 case 'noshade': 6577 case 'nowrap': 6578 if (value === 65535) { 6579 return name; 6580 } 6581 6582 return defaultVal; 6583 6584 case 'shape': 6585 value = value.toLowerCase(); 6586 break; 6587 6588 default: 6589 // IE has odd anonymous function for event attributes 6590 if (name.indexOf('on') === 0 && value) { 6591 value = ('' + value).replace(/^function\s+\w+\(\)\s+\{\s+(.*)\s+\}$/, '$1'); 6592 } 6593 } 6594 } 6595 6596 return (value !== undef && value !== null && value !== '') ? '' + value : defaultVal; 6597 }, 6598 6599 /** 6600 * Returns the absolute x, y position of a node. The position will be returned in an object with x, y fields. 6601 * 6602 * @method getPos 6603 * @param {Element/String} elm HTML element or element id to get x, y position from. 6604 * @param {Element} rootElm Optional root element to stop calculations at. 6605 * @return {object} Absolute position of the specified element object with x, y fields. 6606 */ 6607 getPos: function(elm, rootElm) { 6608 var self = this, x = 0, y = 0, offsetParent, doc = self.doc, pos; 6609 6610 elm = self.get(elm); 6611 rootElm = rootElm || doc.body; 6612 6613 if (elm) { 6614 // Use getBoundingClientRect if it exists since it's faster than looping offset nodes 6615 if (rootElm === doc.body && elm.getBoundingClientRect) { 6616 pos = elm.getBoundingClientRect(); 6617 rootElm = self.boxModel ? doc.documentElement : doc.body; 6618 6619 // Add scroll offsets from documentElement or body since IE with the wrong box model will use d.body and so do WebKit 6620 // Also remove the body/documentelement clientTop/clientLeft on IE 6, 7 since they offset the position 6621 x = pos.left + (doc.documentElement.scrollLeft || doc.body.scrollLeft) - rootElm.clientLeft; 6622 y = pos.top + (doc.documentElement.scrollTop || doc.body.scrollTop) - rootElm.clientTop; 6623 6624 return {x: x, y: y}; 6625 } 6626 6627 offsetParent = elm; 6628 while (offsetParent && offsetParent != rootElm && offsetParent.nodeType) { 6629 x += offsetParent.offsetLeft || 0; 6630 y += offsetParent.offsetTop || 0; 6631 offsetParent = offsetParent.offsetParent; 6632 } 6633 6634 offsetParent = elm.parentNode; 6635 while (offsetParent && offsetParent != rootElm && offsetParent.nodeType) { 6636 x -= offsetParent.scrollLeft || 0; 6637 y -= offsetParent.scrollTop || 0; 6638 offsetParent = offsetParent.parentNode; 6639 } 6640 } 6641 6642 return {x: x, y: y}; 6643 }, 6644 6645 /** 6646 * Parses the specified style value into an object collection. This parser will also 6647 * merge and remove any redundant items that browsers might have added. It will also convert non-hex 6648 * colors to hex values. Urls inside the styles will also be converted to absolute/relative based on settings. 6649 * 6650 * @method parseStyle 6651 * @param {String} cssText Style value to parse, for example: border:1px solid red;. 6652 * @return {Object} Object representation of that style, for example: {border: '1px solid red'} 6653 */ 6654 parseStyle: function(cssText) { 6655 return this.styles.parse(cssText); 6656 }, 6657 6658 /** 6659 * Serializes the specified style object into a string. 6660 * 6661 * @method serializeStyle 6662 * @param {Object} styles Object to serialize as string, for example: {border: '1px solid red'} 6663 * @param {String} name Optional element name. 6664 * @return {String} String representation of the style object, for example: border: 1px solid red. 6665 */ 6666 serializeStyle: function(styles, name) { 6667 return this.styles.serialize(styles, name); 6668 }, 6669 6670 /** 6671 * Adds a style element at the top of the document with the specified cssText content. 6672 * 6673 * @method addStyle 6674 * @param {String} cssText CSS Text style to add to top of head of document. 6675 */ 6676 addStyle: function(cssText) { 6677 var self = this, doc = self.doc, head, styleElm; 6678 6679 // Prevent inline from loading the same styles twice 6680 if (self !== DOMUtils.DOM && doc === document) { 6681 var addedStyles = DOMUtils.DOM.addedStyles; 6682 6683 addedStyles = addedStyles || []; 6684 if (addedStyles[cssText]) { 6685 return; 6686 } 6687 6688 addedStyles[cssText] = true; 6689 DOMUtils.DOM.addedStyles = addedStyles; 6690 } 6691 6692 // Create style element if needed 6693 styleElm = doc.getElementById('mceDefaultStyles'); 6694 if (!styleElm) { 6695 styleElm = doc.createElement('style'); 6696 styleElm.id = 'mceDefaultStyles'; 6697 styleElm.type = 'text/css'; 6698 6699 head = doc.getElementsByTagName('head')[0]; 6700 if (head.firstChild) { 6701 head.insertBefore(styleElm, head.firstChild); 6702 } else { 6703 head.appendChild(styleElm); 6704 } 6705 } 6706 6707 // Append style data to old or new style element 6708 if (styleElm.styleSheet) { 6709 styleElm.styleSheet.cssText += cssText; 6710 } else { 6711 styleElm.appendChild(doc.createTextNode(cssText)); 6712 } 6713 }, 6714 6715 /** 6716 * Imports/loads the specified CSS file into the document bound to the class. 6717 * 6718 * @method loadCSS 6719 * @param {String} u URL to CSS file to load. 6720 * @example 6721 * // Loads a CSS file dynamically into the current document 6722 * tinymce.DOM.loadCSS('somepath/some.css'); 6723 * 6724 * // Loads a CSS file into the currently active editor instance 6725 * tinymce.activeEditor.dom.loadCSS('somepath/some.css'); 6726 * 6727 * // Loads a CSS file into an editor instance by id 6728 * tinymce.get('someid').dom.loadCSS('somepath/some.css'); 6729 * 6730 * // Loads multiple CSS files into the current document 6731 * tinymce.DOM.loadCSS('somepath/some.css,somepath/someother.css'); 6732 */ 6733 loadCSS: function(url) { 6734 var self = this, doc = self.doc, head; 6735 6736 // Prevent inline from loading the same CSS file twice 6737 if (self !== DOMUtils.DOM && doc === document) { 6738 DOMUtils.DOM.loadCSS(url); 6739 return; 6740 } 6741 6742 if (!url) { 6743 url = ''; 6744 } 6745 6746 head = doc.getElementsByTagName('head')[0]; 6747 6748 each(url.split(','), function(url) { 6749 var link; 6750 6751 if (self.files[url]) { 6752 return; 6753 } 6754 6755 self.files[url] = true; 6756 link = self.create('link', {rel: 'stylesheet', href: url}); 6757 6758 // IE 8 has a bug where dynamically loading stylesheets would produce a 1 item remaining bug 6759 // This fix seems to resolve that issue by recalcing the document once a stylesheet finishes loading 6760 // It's ugly but it seems to work fine. 6761 if (isIE && doc.documentMode && doc.recalc) { 6762 link.onload = function() { 6763 if (doc.recalc) { 6764 doc.recalc(); 6765 } 6766 6767 link.onload = null; 6768 }; 6769 } 6770 6771 head.appendChild(link); 6772 }); 6773 }, 6774 6775 /** 6776 * Adds a class to the specified element or elements. 6777 * 6778 * @method addClass 6779 * @param {String/Element/Array} elm Element ID string or DOM element or array with elements or IDs. 6780 * @param {String} cls Class name to add to each element. 6781 * @return {String/Array} String with new class value or array with new class values for all elements. 6782 * @example 6783 * // Adds a class to all paragraphs in the active editor 6784 * tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('p'), 'myclass'); 6785 * 6786 * // Adds a class to a specific element in the current page 6787 * tinymce.DOM.addClass('mydiv', 'myclass'); 6788 */ 6789 addClass: function(elm, cls) { 6790 return this.run(elm, function(elm) { 6791 var clsVal; 6792 6793 if (!cls) { 6794 return 0; 6795 } 6796 6797 if (this.hasClass(elm, cls)) { 6798 return elm.className; 6799 } 6800 6801 clsVal = this.removeClass(elm, cls); 6802 elm.className = clsVal = (clsVal !== '' ? (clsVal + ' ') : '') + cls; 6803 6804 return clsVal; 6805 }); 6806 }, 6807 6808 /** 6809 * Removes a class from the specified element or elements. 6810 * 6811 * @method removeClass 6812 * @param {String/Element/Array} elm Element ID string or DOM element or array with elements or IDs. 6813 * @param {String} cls Class name to remove from each element. 6814 * @return {String/Array} String of remaining class name(s), or an array of strings if multiple input elements 6815 * were passed in. 6816 * @example 6817 * // Removes a class from all paragraphs in the active editor 6818 * tinymce.activeEditor.dom.removeClass(tinymce.activeEditor.dom.select('p'), 'myclass'); 6819 * 6820 * // Removes a class from a specific element in the current page 6821 * tinymce.DOM.removeClass('mydiv', 'myclass'); 6822 */ 6823 removeClass: function(elm, cls) { 6824 var self = this, re; 6825 6826 return self.run(elm, function(elm) { 6827 var val; 6828 6829 if (self.hasClass(elm, cls)) { 6830 if (!re) { 6831 re = new RegExp("(^|\\s+)" + cls + "(\\s+|$)", "g"); 6832 } 6833 6834 val = elm.className.replace(re, ' '); 6835 val = trim(val != ' ' ? val : ''); 6836 6837 elm.className = val; 6838 6839 // Empty class attr 6840 if (!val) { 6841 elm.removeAttribute('class'); 6842 elm.removeAttribute('className'); 6843 } 6844 6845 return val; 6846 } 6847 6848 return elm.className; 6849 }); 6850 }, 6851 6852 /** 6853 * Returns true if the specified element has the specified class. 6854 * 6855 * @method hasClass 6856 * @param {String/Element} n HTML element or element id string to check CSS class on. 6857 * @param {String} c CSS class to check for. 6858 * @return {Boolean} true/false if the specified element has the specified class. 6859 */ 6860 hasClass: function(elm, cls) { 6861 elm = this.get(elm); 6862 6863 if (!elm || !cls) { 6864 return false; 6865 } 6866 6867 return (' ' + elm.className + ' ').indexOf(' ' + cls + ' ') !== -1; 6868 }, 6869 6870 /** 6871 * Toggles the specified class on/off. 6872 * 6873 * @method toggleClass 6874 * @param {Element} elm Element to toggle class on. 6875 * @param {[type]} cls Class to toggle on/off. 6876 * @param {[type]} state Optional state to set. 6877 */ 6878 toggleClass: function(elm, cls, state) { 6879 state = state === undefined ? !this.hasClass(elm, cls) : state; 6880 6881 if (this.hasClass(elm, cls) !== state) { 6882 if (state) { 6883 this.addClass(elm, cls); 6884 } else { 6885 this.removeClass(elm, cls); 6886 } 6887 } 6888 }, 6889 6890 /** 6891 * Shows the specified element(s) by ID by setting the "display" style. 6892 * 6893 * @method show 6894 * @param {String/Element/Array} elm ID of DOM element or DOM element or array with elements or IDs to show. 6895 */ 6896 show: function(elm) { 6897 return this.setStyle(elm, 'display', 'block'); 6898 }, 6899 6900 /** 6901 * Hides the specified element(s) by ID by setting the "display" style. 6902 * 6903 * @method hide 6904 * @param {String/Element/Array} e ID of DOM element or DOM element or array with elements or IDs to hide. 6905 * @example 6906 * // Hides an element by id in the document 6907 * tinymce.DOM.hide('myid'); 6908 */ 6909 hide: function(elm) { 6910 return this.setStyle(elm, 'display', 'none'); 6911 }, 6912 6913 /** 6914 * Returns true/false if the element is hidden or not by checking the "display" style. 6915 * 6916 * @method isHidden 6917 * @param {String/Element} e Id or element to check display state on. 6918 * @return {Boolean} true/false if the element is hidden or not. 6919 */ 6920 isHidden: function(elm) { 6921 elm = this.get(elm); 6922 6923 return !elm || elm.style.display == 'none' || this.getStyle(elm, 'display') == 'none'; 6924 }, 6925 6926 /** 6927 * Returns a unique id. This can be useful when generating elements on the fly. 6928 * This method will not check if the element already exists. 6929 * 6930 * @method uniqueId 6931 * @param {String} prefix Optional prefix to add in front of all ids - defaults to "mce_". 6932 * @return {String} Unique id. 6933 */ 6934 uniqueId: function(prefix) { 6935 return (!prefix ? 'mce_' : prefix) + (this.counter++); 6936 }, 6937 6938 /** 6939 * Sets the specified HTML content inside the element or elements. The HTML will first be processed. This means 6940 * URLs will get converted, hex color values fixed etc. Check processHTML for details. 6941 * 6942 * @method setHTML 6943 * @param {Element/String/Array} e DOM element, element id string or array of elements/ids to set HTML inside of. 6944 * @param {String} h HTML content to set as inner HTML of the element. 6945 * @example 6946 * // Sets the inner HTML of all paragraphs in the active editor 6947 * tinymce.activeEditor.dom.setHTML(tinymce.activeEditor.dom.select('p'), 'some inner html'); 6948 * 6949 * // Sets the inner HTML of an element by id in the document 6950 * tinymce.DOM.setHTML('mydiv', 'some inner html'); 6951 */ 6952 setHTML: function(element, html) { 6953 var self = this; 6954 6955 return self.run(element, function(element) { 6956 if (isIE) { 6957 // Remove all child nodes, IE keeps empty text nodes in DOM 6958 while (element.firstChild) { 6959 element.removeChild(element.firstChild); 6960 } 6961 6962 try { 6963 // IE will remove comments from the beginning 6964 // unless you padd the contents with something 6965 element.innerHTML = '<br />' + html; 6966 element.removeChild(element.firstChild); 6967 } catch (ex) { 6968 // IE sometimes produces an unknown runtime error on innerHTML if it's a block element 6969 // within a block element for example a div inside a p 6970 // This seems to fix this problem 6971 6972 // Create new div with HTML contents and a BR in front to keep comments 6973 var newElement = self.create('div'); 6974 newElement.innerHTML = '<br />' + html; 6975 6976 // Add all children from div to target 6977 each(grep(newElement.childNodes), function(node, i) { 6978 // Skip br element 6979 if (i && element.canHaveHTML) { 6980 element.appendChild(node); 6981 } 6982 }); 6983 } 6984 } else { 6985 element.innerHTML = html; 6986 } 6987 6988 return html; 6989 }); 6990 }, 6991 6992 /** 6993 * Returns the outer HTML of an element. 6994 * 6995 * @method getOuterHTML 6996 * @param {String/Element} elm Element ID or element object to get outer HTML from. 6997 * @return {String} Outer HTML string. 6998 * @example 6999 * tinymce.DOM.getOuterHTML(editorElement); 7000 * tinymce.activeEditor.getOuterHTML(tinymce.activeEditor.getBody()); 7001 */ 7002 getOuterHTML: function(elm) { 7003 var doc, self = this; 7004 7005 elm = self.get(elm); 7006 7007 if (!elm) { 7008 return null; 7009 } 7010 7011 if (elm.nodeType === 1 && self.hasOuterHTML) { 7012 return elm.outerHTML; 7013 } 7014 7015 doc = (elm.ownerDocument || self.doc).createElement("body"); 7016 doc.appendChild(elm.cloneNode(true)); 7017 7018 return doc.innerHTML; 7019 }, 7020 7021 /** 7022 * Sets the specified outer HTML on an element or elements. 7023 * 7024 * @method setOuterHTML 7025 * @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set outer HTML on. 7026 * @param {Object} html HTML code to set as outer value for the element. 7027 * @param {Document} doc Optional document scope to use in this process - defaults to the document of the DOM class. 7028 * @example 7029 * // Sets the outer HTML of all paragraphs in the active editor 7030 * tinymce.activeEditor.dom.setOuterHTML(tinymce.activeEditor.dom.select('p'), '<div>some html</div>'); 7031 * 7032 * // Sets the outer HTML of an element by id in the document 7033 * tinymce.DOM.setOuterHTML('mydiv', '<div>some html</div>'); 7034 */ 7035 setOuterHTML: function(elm, html, doc) { 7036 var self = this; 7037 7038 return self.run(elm, function(elm) { 7039 function set() { 7040 var node, tempElm; 7041 7042 tempElm = doc.createElement("body"); 7043 tempElm.innerHTML = html; 7044 7045 node = tempElm.lastChild; 7046 while (node) { 7047 self.insertAfter(node.cloneNode(true), elm); 7048 node = node.previousSibling; 7049 } 7050 7051 self.remove(elm); 7052 } 7053 7054 // Only set HTML on elements 7055 if (elm.nodeType == 1) { 7056 doc = doc || elm.ownerDocument || self.doc; 7057 7058 if (isIE) { 7059 try { 7060 // Try outerHTML for IE it sometimes produces an unknown runtime error 7061 if (elm.nodeType == 1 && self.hasOuterHTML) { 7062 elm.outerHTML = html; 7063 } else { 7064 set(); 7065 } 7066 } catch (ex) { 7067 // Fix for unknown runtime error 7068 set(); 7069 } 7070 } else { 7071 set(); 7072 } 7073 } 7074 }); 7075 }, 7076 7077 /** 7078 * Entity decodes a string. This method decodes any HTML entities, such as å. 7079 * 7080 * @method decode 7081 * @param {String} s String to decode entities on. 7082 * @return {String} Entity decoded string. 7083 */ 7084 decode: Entities.decode, 7085 7086 /** 7087 * Entity encodes a string. This method encodes the most common entities, such as <>"&. 7088 * 7089 * @method encode 7090 * @param {String} text String to encode with entities. 7091 * @return {String} Entity encoded string. 7092 */ 7093 encode: Entities.encodeAllRaw, 7094 7095 /** 7096 * Inserts an element after the reference element. 7097 * 7098 * @method insertAfter 7099 * @param {Element} node Element to insert after the reference. 7100 * @param {Element/String/Array} reference_node Reference element, element id or array of elements to insert after. 7101 * @return {Element/Array} Element that got added or an array with elements. 7102 */ 7103 insertAfter: function(node, reference_node) { 7104 reference_node = this.get(reference_node); 7105 7106 return this.run(node, function(node) { 7107 var parent, nextSibling; 7108 7109 parent = reference_node.parentNode; 7110 nextSibling = reference_node.nextSibling; 7111 7112 if (nextSibling) { 7113 parent.insertBefore(node, nextSibling); 7114 } else { 7115 parent.appendChild(node); 7116 } 7117 7118 return node; 7119 }); 7120 }, 7121 7122 /** 7123 * Replaces the specified element or elements with the new element specified. The new element will 7124 * be cloned if multiple input elements are passed in. 7125 * 7126 * @method replace 7127 * @param {Element} newElm New element to replace old ones with. 7128 * @param {Element/String/Array} oldELm Element DOM node, element id or array of elements or ids to replace. 7129 * @param {Boolean} k Optional keep children state, if set to true child nodes from the old object will be added to new ones. 7130 */ 7131 replace: function(newElm, oldElm, keepChildren) { 7132 var self = this; 7133 7134 return self.run(oldElm, function(oldElm) { 7135 if (is(oldElm, 'array')) { 7136 newElm = newElm.cloneNode(true); 7137 } 7138 7139 if (keepChildren) { 7140 each(grep(oldElm.childNodes), function(node) { 7141 newElm.appendChild(node); 7142 }); 7143 } 7144 7145 return oldElm.parentNode.replaceChild(newElm, oldElm); 7146 }); 7147 }, 7148 7149 /** 7150 * Renames the specified element and keeps its attributes and children. 7151 * 7152 * @method rename 7153 * @param {Element} elm Element to rename. 7154 * @param {String} name Name of the new element. 7155 * @return {Element} New element or the old element if it needed renaming. 7156 */ 7157 rename: function(elm, name) { 7158 var self = this, newElm; 7159 7160 if (elm.nodeName != name.toUpperCase()) { 7161 // Rename block element 7162 newElm = self.create(name); 7163 7164 // Copy attribs to new block 7165 each(self.getAttribs(elm), function(attr_node) { 7166 self.setAttrib(newElm, attr_node.nodeName, self.getAttrib(elm, attr_node.nodeName)); 7167 }); 7168 7169 // Replace block 7170 self.replace(newElm, elm, 1); 7171 } 7172 7173 return newElm || elm; 7174 }, 7175 7176 /** 7177 * Find the common ancestor of two elements. This is a shorter method than using the DOM Range logic. 7178 * 7179 * @method findCommonAncestor 7180 * @param {Element} a Element to find common ancestor of. 7181 * @param {Element} b Element to find common ancestor of. 7182 * @return {Element} Common ancestor element of the two input elements. 7183 */ 7184 findCommonAncestor: function(a, b) { 7185 var ps = a, pe; 7186 7187 while (ps) { 7188 pe = b; 7189 7190 while (pe && ps != pe) { 7191 pe = pe.parentNode; 7192 } 7193 7194 if (ps == pe) { 7195 break; 7196 } 7197 7198 ps = ps.parentNode; 7199 } 7200 7201 if (!ps && a.ownerDocument) { 7202 return a.ownerDocument.documentElement; 7203 } 7204 7205 return ps; 7206 }, 7207 7208 /** 7209 * Parses the specified RGB color value and returns a hex version of that color. 7210 * 7211 * @method toHex 7212 * @param {String} rgbVal RGB string value like rgb(1,2,3) 7213 * @return {String} Hex version of that RGB value like #FF00FF. 7214 */ 7215 toHex: function(rgbVal) { 7216 return this.styles.toHex(Tools.trim(rgbVal)); 7217 }, 7218 7219 /** 7220 * Executes the specified function on the element by id or dom element node or array of elements/id. 7221 * 7222 * @method run 7223 * @param {String/Element/Array} Element ID or DOM element object or array with ids or elements. 7224 * @param {function} f Function to execute for each item. 7225 * @param {Object} s Optional scope to execute the function in. 7226 * @return {Object/Array} Single object, or an array of objects if multiple input elements were passed in. 7227 */ 7228 run: function(elm, func, scope) { 7229 var self = this, result; 7230 7231 if (typeof(elm) === 'string') { 7232 elm = self.get(elm); 7233 } 7234 7235 if (!elm) { 7236 return false; 7237 } 7238 7239 scope = scope || this; 7240 if (!elm.nodeType && (elm.length || elm.length === 0)) { 7241 result = []; 7242 7243 each(elm, function(elm, i) { 7244 if (elm) { 7245 if (typeof(elm) == 'string') { 7246 elm = self.get(elm); 7247 } 7248 7249 result.push(func.call(scope, elm, i)); 7250 } 7251 }); 7252 7253 return result; 7254 } 7255 7256 return func.call(scope, elm); 7257 }, 7258 7259 /** 7260 * Returns a NodeList with attributes for the element. 7261 * 7262 * @method getAttribs 7263 * @param {HTMLElement/string} elm Element node or string id to get attributes from. 7264 * @return {NodeList} NodeList with attributes. 7265 */ 7266 getAttribs: function(elm) { 7267 var attrs; 7268 7269 elm = this.get(elm); 7270 7271 if (!elm) { 7272 return []; 7273 } 7274 7275 if (isIE) { 7276 attrs = []; 7277 7278 // Object will throw exception in IE 7279 if (elm.nodeName == 'OBJECT') { 7280 return elm.attributes; 7281 } 7282 7283 // IE doesn't keep the selected attribute if you clone option elements 7284 if (elm.nodeName === 'OPTION' && this.getAttrib(elm, 'selected')) { 7285 attrs.push({specified: 1, nodeName: 'selected'}); 7286 } 7287 7288 // It's crazy that this is faster in IE but it's because it returns all attributes all the time 7289 var attrRegExp = /<\/?[\w:\-]+ ?|=[\"][^\"]+\"|=\'[^\']+\'|=[\w\-]+|>/gi; 7290 elm.cloneNode(false).outerHTML.replace(attrRegExp, '').replace(/[\w:\-]+/gi, function(a) { 7291 attrs.push({specified: 1, nodeName: a}); 7292 }); 7293 7294 return attrs; 7295 } 7296 7297 return elm.attributes; 7298 }, 7299 7300 /** 7301 * Returns true/false if the specified node is to be considered empty or not. 7302 * 7303 * @example 7304 * tinymce.DOM.isEmpty(node, {img: true}); 7305 * @method isEmpty 7306 * @param {Object} elements Optional name/value object with elements that are automatically treated as non-empty elements. 7307 * @return {Boolean} true/false if the node is empty or not. 7308 */ 7309 isEmpty: function(node, elements) { 7310 var self = this, i, attributes, type, walker, name, brCount = 0; 7311 7312 node = node.firstChild; 7313 if (node) { 7314 walker = new TreeWalker(node, node.parentNode); 7315 elements = elements || self.schema ? self.schema.getNonEmptyElements() : null; 7316 7317 do { 7318 type = node.nodeType; 7319 7320 if (type === 1) { 7321 // Ignore bogus elements 7322 if (node.getAttribute('data-mce-bogus')) { 7323 continue; 7324 } 7325 7326 // Keep empty elements like <img /> 7327 name = node.nodeName.toLowerCase(); 7328 if (elements && elements[name]) { 7329 // Ignore single BR elements in blocks like <p><br /></p> or <p><span><br /></span></p> 7330 if (name === 'br') { 7331 brCount++; 7332 continue; 7333 } 7334 7335 return false; 7336 } 7337 7338 // Keep elements with data-bookmark attributes or name attribute like <a name="1"></a> 7339 attributes = self.getAttribs(node); 7340 i = attributes.length; 7341 while (i--) { 7342 name = attributes[i].nodeName; 7343 if (name === "name" || name === 'data-mce-bookmark') { 7344 return false; 7345 } 7346 } 7347 } 7348 7349 // Keep comment nodes 7350 if (type == 8) { 7351 return false; 7352 } 7353 7354 // Keep non whitespace text nodes 7355 if ((type === 3 && !whiteSpaceRegExp.test(node.nodeValue))) { 7356 return false; 7357 } 7358 } while ((node = walker.next())); 7359 } 7360 7361 return brCount <= 1; 7362 }, 7363 7364 /** 7365 * Creates a new DOM Range object. This will use the native DOM Range API if it's 7366 * available. If it's not, it will fall back to the custom TinyMCE implementation. 7367 * 7368 * @method createRng 7369 * @return {DOMRange} DOM Range object. 7370 * @example 7371 * var rng = tinymce.DOM.createRng(); 7372 * alert(rng.startContainer + "," + rng.startOffset); 7373 */ 7374 createRng: function() { 7375 var doc = this.doc; 7376 7377 return doc.createRange ? doc.createRange() : new Range(this); 7378 }, 7379 7380 /** 7381 * Returns the index of the specified node within its parent. 7382 * 7383 * @method nodeIndex 7384 * @param {Node} node Node to look for. 7385 * @param {boolean} normalized Optional true/false state if the index is what it would be after a normalization. 7386 * @return {Number} Index of the specified node. 7387 */ 7388 nodeIndex: function(node, normalized) { 7389 var idx = 0, lastNodeType, nodeType; 7390 7391 if (node) { 7392 for (lastNodeType = node.nodeType, node = node.previousSibling; node; node = node.previousSibling) { 7393 nodeType = node.nodeType; 7394 7395 // Normalize text nodes 7396 if (normalized && nodeType == 3) { 7397 if (nodeType == lastNodeType || !node.nodeValue.length) { 7398 continue; 7399 } 7400 } 7401 idx++; 7402 lastNodeType = nodeType; 7403 } 7404 } 7405 7406 return idx; 7407 }, 7408 7409 /** 7410 * Splits an element into two new elements and places the specified split 7411 * element or elements between the new ones. For example splitting the paragraph at the bold element in 7412 * this example <p>abc<b>abc</b>123</p> would produce <p>abc</p><b>abc</b><p>123</p>. 7413 * 7414 * @method split 7415 * @param {Element} parentElm Parent element to split. 7416 * @param {Element} splitElm Element to split at. 7417 * @param {Element} replacementElm Optional replacement element to replace the split element with. 7418 * @return {Element} Returns the split element or the replacement element if that is specified. 7419 */ 7420 split: function(parentElm, splitElm, replacementElm) { 7421 var self = this, r = self.createRng(), bef, aft, pa; 7422 7423 // W3C valid browsers tend to leave empty nodes to the left/right side of the contents - this makes sense 7424 // but we don't want that in our code since it serves no purpose for the end user 7425 // For example splitting this html at the bold element: 7426 // <p>text 1<span><b>CHOP</b></span>text 2</p> 7427 // would produce: 7428 // <p>text 1<span></span></p><b>CHOP</b><p><span></span>text 2</p> 7429 // this function will then trim off empty edges and produce: 7430 // <p>text 1</p><b>CHOP</b><p>text 2</p> 7431 function trimNode(node) { 7432 var i, children = node.childNodes, type = node.nodeType; 7433 7434 function surroundedBySpans(node) { 7435 var previousIsSpan = node.previousSibling && node.previousSibling.nodeName == 'SPAN'; 7436 var nextIsSpan = node.nextSibling && node.nextSibling.nodeName == 'SPAN'; 7437 return previousIsSpan && nextIsSpan; 7438 } 7439 7440 if (type == 1 && node.getAttribute('data-mce-type') == 'bookmark') { 7441 return; 7442 } 7443 7444 for (i = children.length - 1; i >= 0; i--) { 7445 trimNode(children[i]); 7446 } 7447 7448 if (type != 9) { 7449 // Keep non whitespace text nodes 7450 if (type == 3 && node.nodeValue.length > 0) { 7451 // If parent element isn't a block or there isn't any useful contents for example "<p> </p>" 7452 // Also keep text nodes with only spaces if surrounded by spans. 7453 // eg. "<p><span>a</span> <span>b</span></p>" should keep space between a and b 7454 var trimmedLength = trim(node.nodeValue).length; 7455 if (!self.isBlock(node.parentNode) || trimmedLength > 0 || trimmedLength === 0 && surroundedBySpans(node)) { 7456 return; 7457 } 7458 } else if (type == 1) { 7459 // If the only child is a bookmark then move it up 7460 children = node.childNodes; 7461 7462 // TODO fix this complex if 7463 if (children.length == 1 && children[0] && children[0].nodeType == 1 && 7464 children[0].getAttribute('data-mce-type') == 'bookmark') { 7465 node.parentNode.insertBefore(children[0], node); 7466 } 7467 7468 // Keep non empty elements or img, hr etc 7469 if (children.length || /^(br|hr|input|img)$/i.test(node.nodeName)) { 7470 return; 7471 } 7472 } 7473 7474 self.remove(node); 7475 } 7476 7477 return node; 7478 } 7479 7480 if (parentElm && splitElm) { 7481 // Get before chunk 7482 r.setStart(parentElm.parentNode, self.nodeIndex(parentElm)); 7483 r.setEnd(splitElm.parentNode, self.nodeIndex(splitElm)); 7484 bef = r.extractContents(); 7485 7486 // Get after chunk 7487 r = self.createRng(); 7488 r.setStart(splitElm.parentNode, self.nodeIndex(splitElm) + 1); 7489 r.setEnd(parentElm.parentNode, self.nodeIndex(parentElm) + 1); 7490 aft = r.extractContents(); 7491 7492 // Insert before chunk 7493 pa = parentElm.parentNode; 7494 pa.insertBefore(trimNode(bef), parentElm); 7495 7496 // Insert middle chunk 7497 if (replacementElm) { 7498 pa.replaceChild(replacementElm, splitElm); 7499 } else { 7500 pa.insertBefore(splitElm, parentElm); 7501 } 7502 7503 // Insert after chunk 7504 pa.insertBefore(trimNode(aft), parentElm); 7505 self.remove(parentElm); 7506 7507 return replacementElm || splitElm; 7508 } 7509 }, 7510 7511 /** 7512 * Adds an event handler to the specified object. 7513 * 7514 * @method bind 7515 * @param {Element/Document/Window/Array} target Target element to bind events to. 7516 * handler to or an array of elements/ids/documents. 7517 * @param {String} name Name of event handler to add, for example: click. 7518 * @param {function} func Function to execute when the event occurs. 7519 * @param {Object} scope Optional scope to execute the function in. 7520 * @return {function} Function callback handler the same as the one passed in. 7521 */ 7522 bind: function(target, name, func, scope) { 7523 var self = this; 7524 7525 if (Tools.isArray(target)) { 7526 var i = target.length; 7527 7528 while (i--) { 7529 target[i] = self.bind(target[i], name, func, scope); 7530 } 7531 7532 return target; 7533 } 7534 7535 // Collect all window/document events bound by editor instance 7536 if (self.settings.collect && (target === self.doc || target === self.win)) { 7537 self.boundEvents.push([target, name, func, scope]); 7538 } 7539 7540 return self.events.bind(target, name, func, scope || self); 7541 }, 7542 7543 /** 7544 * Removes the specified event handler by name and function from an element or collection of elements. 7545 * 7546 * @method unbind 7547 * @param {Element/Document/Window/Array} target Target element to unbind events on. 7548 * @param {String} name Event handler name, for example: "click" 7549 * @param {function} func Function to remove. 7550 * @return {bool/Array} Bool state of true if the handler was removed, or an array of states if multiple input elements 7551 * were passed in. 7552 */ 7553 unbind: function(target, name, func) { 7554 var self = this, i; 7555 7556 if (Tools.isArray(target)) { 7557 i = target.length; 7558 7559 while (i--) { 7560 target[i] = self.unbind(target[i], name, func); 7561 } 7562 7563 return target; 7564 } 7565 7566 // Remove any bound events matching the input 7567 if (self.boundEvents && (target === self.doc || target === self.win)) { 7568 i = self.boundEvents.length; 7569 7570 while (i--) { 7571 var item = self.boundEvents[i]; 7572 7573 if (target == item[0] && (!name || name == item[1]) && (!func || func == item[2])) { 7574 this.events.unbind(item[0], item[1], item[2]); 7575 } 7576 } 7577 } 7578 7579 return this.events.unbind(target, name, func); 7580 }, 7581 7582 /** 7583 * Fires the specified event name with object on target. 7584 * 7585 * @method fire 7586 * @param {Node/Document/Window} target Target element or object to fire event on. 7587 * @param {String} name Name of the event to fire. 7588 * @param {Object} evt Event object to send. 7589 * @return {Event} Event object. 7590 */ 7591 fire: function(target, name, evt) { 7592 return this.events.fire(target, name, evt); 7593 }, 7594 7595 // Returns the content editable state of a node 7596 getContentEditable: function(node) { 7597 var contentEditable; 7598 7599 // Check type 7600 if (!node || node.nodeType != 1) { 7601 return null; 7602 } 7603 7604 // Check for fake content editable 7605 contentEditable = node.getAttribute("data-mce-contenteditable"); 7606 if (contentEditable && contentEditable !== "inherit") { 7607 return contentEditable; 7608 } 7609 7610 // Check for real content editable 7611 return node.contentEditable !== "inherit" ? node.contentEditable : null; 7612 }, 7613 7614 getContentEditableParent: function(node) { 7615 var root = this.getRoot(), state = null; 7616 7617 for (; node && node !== root; node = node.parentNode) { 7618 state = this.getContentEditable(node); 7619 7620 if (state !== null) { 7621 break; 7622 } 7623 } 7624 7625 return state; 7626 }, 7627 7628 /** 7629 * Destroys all internal references to the DOM to solve IE leak issues. 7630 * 7631 * @method destroy 7632 */ 7633 destroy: function() { 7634 var self = this; 7635 7636 // Unbind all events bound to window/document by editor instance 7637 if (self.boundEvents) { 7638 var i = self.boundEvents.length; 7639 7640 while (i--) { 7641 var item = self.boundEvents[i]; 7642 this.events.unbind(item[0], item[1], item[2]); 7643 } 7644 7645 self.boundEvents = null; 7646 } 7647 7648 // Restore sizzle document to window.document 7649 // Since the current document might be removed producing "Permission denied" on IE see #6325 7650 if (Sizzle.setDocument) { 7651 Sizzle.setDocument(); 7652 } 7653 7654 self.win = self.doc = self.root = self.events = self.frag = null; 7655 }, 7656 7657 isChildOf: function(node, parent) { 7658 if (parent.contains) { 7659 return parent.contains(node); 7660 } 7661 7662 while (node) { 7663 if (parent === node) { 7664 return true; 7665 } 7666 7667 node = node.parentNode; 7668 } 7669 7670 return false; 7671 }, 7672 7673 // #ifdef debug 7674 7675 dumpRng: function(r) { 7676 return ( 7677 'startContainer: ' + r.startContainer.nodeName + 7678 ', startOffset: ' + r.startOffset + 7679 ', endContainer: ' + r.endContainer.nodeName + 7680 ', endOffset: ' + r.endOffset 7681 ); 7682 }, 7683 7684 // #endif 7685 7686 _findSib: function(node, selector, name) { 7687 var self = this, func = selector; 7688 7689 if (node) { 7690 // If expression make a function of it using is 7691 if (typeof(func) == 'string') { 7692 func = function(node) { 7693 return self.is(node, selector); 7694 }; 7695 } 7696 7697 // Loop all siblings 7698 for (node = node[name]; node; node = node[name]) { 7699 if (func(node)) { 7700 return node; 7701 } 7702 } 7703 } 7704 7705 return null; 7706 } 7707 }; 7708 7709 /** 7710 * Instance of DOMUtils for the current document. 7711 * 7712 * @static 7713 * @property DOM 7714 * @type tinymce.dom.DOMUtils 7715 * @example 7716 * // Example of how to add a class to some element by id 7717 * tinymce.DOM.addClass('someid', 'someclass'); 7718 */ 7719 DOMUtils.DOM = new DOMUtils(document); 7720 7721 return DOMUtils; 7722 }); 7723 7724 // Included from: js/tinymce/classes/dom/ScriptLoader.js 7725 7726 /** 7727 * ScriptLoader.js 7728 * 7729 * Copyright, Moxiecode Systems AB 7730 * Released under LGPL License. 7731 * 7732 * License: http://www.tinymce.com/license 7733 * Contributing: http://www.tinymce.com/contributing 7734 */ 7735 7736 /*globals console*/ 7737 7738 /** 7739 * This class handles asynchronous/synchronous loading of JavaScript files it will execute callbacks 7740 * when various items gets loaded. This class is useful to load external JavaScript files. 7741 * 7742 * @class tinymce.dom.ScriptLoader 7743 * @example 7744 * // Load a script from a specific URL using the global script loader 7745 * tinymce.ScriptLoader.load('somescript.js'); 7746 * 7747 * // Load a script using a unique instance of the script loader 7748 * var scriptLoader = new tinymce.dom.ScriptLoader(); 7749 * 7750 * scriptLoader.load('somescript.js'); 7751 * 7752 * // Load multiple scripts 7753 * var scriptLoader = new tinymce.dom.ScriptLoader(); 7754 * 7755 * scriptLoader.add('somescript1.js'); 7756 * scriptLoader.add('somescript2.js'); 7757 * scriptLoader.add('somescript3.js'); 7758 * 7759 * scriptLoader.loadQueue(function() { 7760 * alert('All scripts are now loaded.'); 7761 * }); 7762 */ 7763 define("tinymce/dom/ScriptLoader", [ 7764 "tinymce/dom/DOMUtils", 7765 "tinymce/util/Tools" 7766 ], function(DOMUtils, Tools) { 7767 var DOM = DOMUtils.DOM; 7768 var each = Tools.each, grep = Tools.grep; 7769 7770 function ScriptLoader() { 7771 var QUEUED = 0, 7772 LOADING = 1, 7773 LOADED = 2, 7774 states = {}, 7775 queue = [], 7776 scriptLoadedCallbacks = {}, 7777 queueLoadedCallbacks = [], 7778 loading = 0, 7779 undef; 7780 7781 /** 7782 * Loads a specific script directly without adding it to the load queue. 7783 * 7784 * @method load 7785 * @param {String} url Absolute URL to script to add. 7786 * @param {function} callback Optional callback function to execute ones this script gets loaded. 7787 * @param {Object} scope Optional scope to execute callback in. 7788 */ 7789 function loadScript(url, callback) { 7790 var dom = DOM, elm, id; 7791 7792 // Execute callback when script is loaded 7793 function done() { 7794 dom.remove(id); 7795 7796 if (elm) { 7797 elm.onreadystatechange = elm.onload = elm = null; 7798 } 7799 7800 callback(); 7801 } 7802 7803 function error() { 7804 /*eslint no-console:0 */ 7805 7806 // Report the error so it's easier for people to spot loading errors 7807 if (typeof(console) !== "undefined" && console.log) { 7808 console.log("Failed to load: " + url); 7809 } 7810 7811 // We can't mark it as done if there is a load error since 7812 // A) We don't want to produce 404 errors on the server and 7813 // B) the onerror event won't fire on all browsers. 7814 // done(); 7815 } 7816 7817 id = dom.uniqueId(); 7818 7819 // Create new script element 7820 elm = document.createElement('script'); 7821 elm.id = id; 7822 elm.type = 'text/javascript'; 7823 elm.src = url; 7824 7825 // Seems that onreadystatechange works better on IE 10 onload seems to fire incorrectly 7826 if ("onreadystatechange" in elm) { 7827 elm.onreadystatechange = function() { 7828 if (/loaded|complete/.test(elm.readyState)) { 7829 done(); 7830 } 7831 }; 7832 } else { 7833 elm.onload = done; 7834 } 7835 7836 // Add onerror event will get fired on some browsers but not all of them 7837 elm.onerror = error; 7838 7839 // Add script to document 7840 (document.getElementsByTagName('head')[0] || document.body).appendChild(elm); 7841 } 7842 7843 /** 7844 * Returns true/false if a script has been loaded or not. 7845 * 7846 * @method isDone 7847 * @param {String} url URL to check for. 7848 * @return {Boolean} true/false if the URL is loaded. 7849 */ 7850 this.isDone = function(url) { 7851 return states[url] == LOADED; 7852 }; 7853 7854 /** 7855 * Marks a specific script to be loaded. This can be useful if a script got loaded outside 7856 * the script loader or to skip it from loading some script. 7857 * 7858 * @method markDone 7859 * @param {string} u Absolute URL to the script to mark as loaded. 7860 */ 7861 this.markDone = function(url) { 7862 states[url] = LOADED; 7863 }; 7864 7865 /** 7866 * Adds a specific script to the load queue of the script loader. 7867 * 7868 * @method add 7869 * @param {String} url Absolute URL to script to add. 7870 * @param {function} callback Optional callback function to execute ones this script gets loaded. 7871 * @param {Object} scope Optional scope to execute callback in. 7872 */ 7873 this.add = this.load = function(url, callback, scope) { 7874 var state = states[url]; 7875 7876 // Add url to load queue 7877 if (state == undef) { 7878 queue.push(url); 7879 states[url] = QUEUED; 7880 } 7881 7882 if (callback) { 7883 // Store away callback for later execution 7884 if (!scriptLoadedCallbacks[url]) { 7885 scriptLoadedCallbacks[url] = []; 7886 } 7887 7888 scriptLoadedCallbacks[url].push({ 7889 func: callback, 7890 scope: scope || this 7891 }); 7892 } 7893 }; 7894 7895 /** 7896 * Starts the loading of the queue. 7897 * 7898 * @method loadQueue 7899 * @param {function} callback Optional callback to execute when all queued items are loaded. 7900 * @param {Object} scope Optional scope to execute the callback in. 7901 */ 7902 this.loadQueue = function(callback, scope) { 7903 this.loadScripts(queue, callback, scope); 7904 }; 7905 7906 /** 7907 * Loads the specified queue of files and executes the callback ones they are loaded. 7908 * This method is generally not used outside this class but it might be useful in some scenarios. 7909 * 7910 * @method loadScripts 7911 * @param {Array} scripts Array of queue items to load. 7912 * @param {function} callback Optional callback to execute ones all items are loaded. 7913 * @param {Object} scope Optional scope to execute callback in. 7914 */ 7915 this.loadScripts = function(scripts, callback, scope) { 7916 var loadScripts; 7917 7918 function execScriptLoadedCallbacks(url) { 7919 // Execute URL callback functions 7920 each(scriptLoadedCallbacks[url], function(callback) { 7921 callback.func.call(callback.scope); 7922 }); 7923 7924 scriptLoadedCallbacks[url] = undef; 7925 } 7926 7927 queueLoadedCallbacks.push({ 7928 func: callback, 7929 scope: scope || this 7930 }); 7931 7932 loadScripts = function() { 7933 var loadingScripts = grep(scripts); 7934 7935 // Current scripts has been handled 7936 scripts.length = 0; 7937 7938 // Load scripts that needs to be loaded 7939 each(loadingScripts, function(url) { 7940 // Script is already loaded then execute script callbacks directly 7941 if (states[url] == LOADED) { 7942 execScriptLoadedCallbacks(url); 7943 return; 7944 } 7945 7946 // Is script not loading then start loading it 7947 if (states[url] != LOADING) { 7948 states[url] = LOADING; 7949 loading++; 7950 7951 loadScript(url, function() { 7952 states[url] = LOADED; 7953 loading--; 7954 7955 execScriptLoadedCallbacks(url); 7956 7957 // Load more scripts if they where added by the recently loaded script 7958 loadScripts(); 7959 }); 7960 } 7961 }); 7962 7963 // No scripts are currently loading then execute all pending queue loaded callbacks 7964 if (!loading) { 7965 each(queueLoadedCallbacks, function(callback) { 7966 callback.func.call(callback.scope); 7967 }); 7968 7969 queueLoadedCallbacks.length = 0; 7970 } 7971 }; 7972 7973 loadScripts(); 7974 }; 7975 } 7976 7977 ScriptLoader.ScriptLoader = new ScriptLoader(); 7978 7979 return ScriptLoader; 7980 }); 7981 7982 // Included from: js/tinymce/classes/AddOnManager.js 7983 7984 /** 7985 * AddOnManager.js 7986 * 7987 * Copyright, Moxiecode Systems AB 7988 * Released under LGPL License. 7989 * 7990 * License: http://www.tinymce.com/license 7991 * Contributing: http://www.tinymce.com/contributing 7992 */ 7993 7994 /** 7995 * This class handles the loading of themes/plugins or other add-ons and their language packs. 7996 * 7997 * @class tinymce.AddOnManager 7998 */ 7999 define("tinymce/AddOnManager", [ 8000 "tinymce/dom/ScriptLoader", 8001 "tinymce/util/Tools" 8002 ], function(ScriptLoader, Tools) { 8003 var each = Tools.each; 8004 8005 function AddOnManager() { 8006 var self = this; 8007 8008 self.items = []; 8009 self.urls = {}; 8010 self.lookup = {}; 8011 } 8012 8013 AddOnManager.prototype = { 8014 /** 8015 * Returns the specified add on by the short name. 8016 * 8017 * @method get 8018 * @param {String} name Add-on to look for. 8019 * @return {tinymce.Theme/tinymce.Plugin} Theme or plugin add-on instance or undefined. 8020 */ 8021 get: function(name) { 8022 if (this.lookup[name]) { 8023 return this.lookup[name].instance; 8024 } else { 8025 return undefined; 8026 } 8027 }, 8028 8029 dependencies: function(name) { 8030 var result; 8031 8032 if (this.lookup[name]) { 8033 result = this.lookup[name].dependencies; 8034 } 8035 8036 return result || []; 8037 }, 8038 8039 /** 8040 * Loads a language pack for the specified add-on. 8041 * 8042 * @method requireLangPack 8043 * @param {String} name Short name of the add-on. 8044 * @param {String} languages Optional comma or space separated list of languages to check if it matches the name. 8045 */ 8046 requireLangPack: function(name, languages) { 8047 var language = AddOnManager.language; 8048 8049 if (language && AddOnManager.languageLoad !== false) { 8050 if (languages) { 8051 languages = ',' + languages + ','; 8052 8053 // Load short form sv.js or long form sv_SE.js 8054 if (languages.indexOf(',' + language.substr(0, 2) + ',') != -1) { 8055 language = language.substr(0, 2); 8056 } else if (languages.indexOf(',' + language + ',') == -1) { 8057 return; 8058 } 8059 } 8060 8061 ScriptLoader.ScriptLoader.add(this.urls[name] + '/langs/' + language + '.js'); 8062 } 8063 }, 8064 8065 /** 8066 * Adds a instance of the add-on by it's short name. 8067 * 8068 * @method add 8069 * @param {String} id Short name/id for the add-on. 8070 * @param {tinymce.Theme/tinymce.Plugin} addOn Theme or plugin to add. 8071 * @return {tinymce.Theme/tinymce.Plugin} The same theme or plugin instance that got passed in. 8072 * @example 8073 * // Create a simple plugin 8074 * tinymce.create('tinymce.plugins.TestPlugin', { 8075 * TestPlugin: function(ed, url) { 8076 * ed.on('click', function(e) { 8077 * ed.windowManager.alert('Hello World!'); 8078 * }); 8079 * } 8080 * }); 8081 * 8082 * // Register plugin using the add method 8083 * tinymce.PluginManager.add('test', tinymce.plugins.TestPlugin); 8084 * 8085 * // Initialize TinyMCE 8086 * tinymce.init({ 8087 * ... 8088 * plugins: '-test' // Init the plugin but don't try to load it 8089 * }); 8090 */ 8091 add: function(id, addOn, dependencies) { 8092 this.items.push(addOn); 8093 this.lookup[id] = {instance: addOn, dependencies: dependencies}; 8094 8095 return addOn; 8096 }, 8097 8098 createUrl: function(baseUrl, dep) { 8099 if (typeof dep === "object") { 8100 return dep; 8101 } else { 8102 return {prefix: baseUrl.prefix, resource: dep, suffix: baseUrl.suffix}; 8103 } 8104 }, 8105 8106 /** 8107 * Add a set of components that will make up the add-on. Using the url of the add-on name as the base url. 8108 * This should be used in development mode. A new compressor/javascript munger process will ensure that the 8109 * components are put together into the plugin.js file and compressed correctly. 8110 * 8111 * @method addComponents 8112 * @param {String} pluginName name of the plugin to load scripts from (will be used to get the base url for the plugins). 8113 * @param {Array} scripts Array containing the names of the scripts to load. 8114 */ 8115 addComponents: function(pluginName, scripts) { 8116 var pluginUrl = this.urls[pluginName]; 8117 8118 each(scripts, function(script) { 8119 ScriptLoader.ScriptLoader.add(pluginUrl + "/" + script); 8120 }); 8121 }, 8122 8123 /** 8124 * Loads an add-on from a specific url. 8125 * 8126 * @method load 8127 * @param {String} name Short name of the add-on that gets loaded. 8128 * @param {String} addOnUrl URL to the add-on that will get loaded. 8129 * @param {function} callback Optional callback to execute ones the add-on is loaded. 8130 * @param {Object} scope Optional scope to execute the callback in. 8131 * @example 8132 * // Loads a plugin from an external URL 8133 * tinymce.PluginManager.load('myplugin', '/some/dir/someplugin/plugin.js'); 8134 * 8135 * // Initialize TinyMCE 8136 * tinymce.init({ 8137 * ... 8138 * plugins: '-myplugin' // Don't try to load it again 8139 * }); 8140 */ 8141 load: function(name, addOnUrl, callback, scope) { 8142 var self = this, url = addOnUrl; 8143 8144 function loadDependencies() { 8145 var dependencies = self.dependencies(name); 8146 8147 each(dependencies, function(dep) { 8148 var newUrl = self.createUrl(addOnUrl, dep); 8149 8150 self.load(newUrl.resource, newUrl, undefined, undefined); 8151 }); 8152 8153 if (callback) { 8154 if (scope) { 8155 callback.call(scope); 8156 } else { 8157 callback.call(ScriptLoader); 8158 } 8159 } 8160 } 8161 8162 if (self.urls[name]) { 8163 return; 8164 } 8165 8166 if (typeof addOnUrl === "object") { 8167 url = addOnUrl.prefix + addOnUrl.resource + addOnUrl.suffix; 8168 } 8169 8170 if (url.indexOf('/') !== 0 && url.indexOf('://') == -1) { 8171 url = AddOnManager.baseURL + '/' + url; 8172 } 8173 8174 self.urls[name] = url.substring(0, url.lastIndexOf('/')); 8175 8176 if (self.lookup[name]) { 8177 loadDependencies(); 8178 } else { 8179 ScriptLoader.ScriptLoader.add(url, loadDependencies, scope); 8180 } 8181 } 8182 }; 8183 8184 AddOnManager.PluginManager = new AddOnManager(); 8185 AddOnManager.ThemeManager = new AddOnManager(); 8186 8187 return AddOnManager; 8188 }); 8189 8190 /** 8191 * TinyMCE theme class. 8192 * 8193 * @class tinymce.Theme 8194 */ 8195 8196 /** 8197 * This method is responsible for rendering/generating the overall user interface with toolbars, buttons, iframe containers etc. 8198 * 8199 * @method renderUI 8200 * @param {Object} obj Object parameter containing the targetNode DOM node that will be replaced visually with an editor instance. 8201 * @return {Object} an object with items like iframeContainer, editorContainer, sizeContainer, deltaWidth, deltaHeight. 8202 */ 8203 8204 /** 8205 * Plugin base class, this is a pseudo class that describes how a plugin is to be created for TinyMCE. The methods below are all optional. 8206 * 8207 * @class tinymce.Plugin 8208 * @example 8209 * tinymce.PluginManager.add('example', function(editor, url) { 8210 * // Add a button that opens a window 8211 * editor.addButton('example', { 8212 * text: 'My button', 8213 * icon: false, 8214 * onclick: function() { 8215 * // Open window 8216 * editor.windowManager.open({ 8217 * title: 'Example plugin', 8218 * body: [ 8219 * {type: 'textbox', name: 'title', label: 'Title'} 8220 * ], 8221 * onsubmit: function(e) { 8222 * // Insert content when the window form is submitted 8223 * editor.insertContent('Title: ' + e.data.title); 8224 * } 8225 * }); 8226 * } 8227 * }); 8228 * 8229 * // Adds a menu item to the tools menu 8230 * editor.addMenuItem('example', { 8231 * text: 'Example plugin', 8232 * context: 'tools', 8233 * onclick: function() { 8234 * // Open window with a specific url 8235 * editor.windowManager.open({ 8236 * title: 'TinyMCE site', 8237 * url: 'http://www.tinymce.com', 8238 * width: 800, 8239 * height: 600, 8240 * buttons: [{ 8241 * text: 'Close', 8242 * onclick: 'close' 8243 * }] 8244 * }); 8245 * } 8246 * }); 8247 * }); 8248 */ 8249 8250 // Included from: js/tinymce/classes/html/Node.js 8251 8252 /** 8253 * Node.js 8254 * 8255 * Copyright, Moxiecode Systems AB 8256 * Released under LGPL License. 8257 * 8258 * License: http://www.tinymce.com/license 8259 * Contributing: http://www.tinymce.com/contributing 8260 */ 8261 8262 /** 8263 * This class is a minimalistic implementation of a DOM like node used by the DomParser class. 8264 * 8265 * @example 8266 * var node = new tinymce.html.Node('strong', 1); 8267 * someRoot.append(node); 8268 * 8269 * @class tinymce.html.Node 8270 * @version 3.4 8271 */ 8272 define("tinymce/html/Node", [], function() { 8273 var whiteSpaceRegExp = /^[ \t\r\n]*$/, typeLookup = { 8274 '#text': 3, 8275 '#comment': 8, 8276 '#cdata': 4, 8277 '#pi': 7, 8278 '#doctype': 10, 8279 '#document-fragment': 11 8280 }; 8281 8282 // Walks the tree left/right 8283 function walk(node, root_node, prev) { 8284 var sibling, parent, startName = prev ? 'lastChild' : 'firstChild', siblingName = prev ? 'prev' : 'next'; 8285 8286 // Walk into nodes if it has a start 8287 if (node[startName]) { 8288 return node[startName]; 8289 } 8290 8291 // Return the sibling if it has one 8292 if (node !== root_node) { 8293 sibling = node[siblingName]; 8294 8295 if (sibling) { 8296 return sibling; 8297 } 8298 8299 // Walk up the parents to look for siblings 8300 for (parent = node.parent; parent && parent !== root_node; parent = parent.parent) { 8301 sibling = parent[siblingName]; 8302 8303 if (sibling) { 8304 return sibling; 8305 } 8306 } 8307 } 8308 } 8309 8310 /** 8311 * Constructs a new Node instance. 8312 * 8313 * @constructor 8314 * @method Node 8315 * @param {String} name Name of the node type. 8316 * @param {Number} type Numeric type representing the node. 8317 */ 8318 function Node(name, type) { 8319 this.name = name; 8320 this.type = type; 8321 8322 if (type === 1) { 8323 this.attributes = []; 8324 this.attributes.map = {}; 8325 } 8326 } 8327 8328 Node.prototype = { 8329 /** 8330 * Replaces the current node with the specified one. 8331 * 8332 * @example 8333 * someNode.replace(someNewNode); 8334 * 8335 * @method replace 8336 * @param {tinymce.html.Node} node Node to replace the current node with. 8337 * @return {tinymce.html.Node} The old node that got replaced. 8338 */ 8339 replace: function(node) { 8340 var self = this; 8341 8342 if (node.parent) { 8343 node.remove(); 8344 } 8345 8346 self.insert(node, self); 8347 self.remove(); 8348 8349 return self; 8350 }, 8351 8352 /** 8353 * Gets/sets or removes an attribute by name. 8354 * 8355 * @example 8356 * someNode.attr("name", "value"); // Sets an attribute 8357 * console.log(someNode.attr("name")); // Gets an attribute 8358 * someNode.attr("name", null); // Removes an attribute 8359 * 8360 * @method attr 8361 * @param {String} name Attribute name to set or get. 8362 * @param {String} value Optional value to set. 8363 * @return {String/tinymce.html.Node} String or undefined on a get operation or the current node on a set operation. 8364 */ 8365 attr: function(name, value) { 8366 var self = this, attrs, i, undef; 8367 8368 if (typeof name !== "string") { 8369 for (i in name) { 8370 self.attr(i, name[i]); 8371 } 8372 8373 return self; 8374 } 8375 8376 if ((attrs = self.attributes)) { 8377 if (value !== undef) { 8378 // Remove attribute 8379 if (value === null) { 8380 if (name in attrs.map) { 8381 delete attrs.map[name]; 8382 8383 i = attrs.length; 8384 while (i--) { 8385 if (attrs[i].name === name) { 8386 attrs = attrs.splice(i, 1); 8387 return self; 8388 } 8389 } 8390 } 8391 8392 return self; 8393 } 8394 8395 // Set attribute 8396 if (name in attrs.map) { 8397 // Set attribute 8398 i = attrs.length; 8399 while (i--) { 8400 if (attrs[i].name === name) { 8401 attrs[i].value = value; 8402 break; 8403 } 8404 } 8405 } else { 8406 attrs.push({name: name, value: value}); 8407 } 8408 8409 attrs.map[name] = value; 8410 8411 return self; 8412 } else { 8413 return attrs.map[name]; 8414 } 8415 } 8416 }, 8417 8418 /** 8419 * Does a shallow clones the node into a new node. It will also exclude id attributes since 8420 * there should only be one id per document. 8421 * 8422 * @example 8423 * var clonedNode = node.clone(); 8424 * 8425 * @method clone 8426 * @return {tinymce.html.Node} New copy of the original node. 8427 */ 8428 clone: function() { 8429 var self = this, clone = new Node(self.name, self.type), i, l, selfAttrs, selfAttr, cloneAttrs; 8430 8431 // Clone element attributes 8432 if ((selfAttrs = self.attributes)) { 8433 cloneAttrs = []; 8434 cloneAttrs.map = {}; 8435 8436 for (i = 0, l = selfAttrs.length; i < l; i++) { 8437 selfAttr = selfAttrs[i]; 8438 8439 // Clone everything except id 8440 if (selfAttr.name !== 'id') { 8441 cloneAttrs[cloneAttrs.length] = {name: selfAttr.name, value: selfAttr.value}; 8442 cloneAttrs.map[selfAttr.name] = selfAttr.value; 8443 } 8444 } 8445 8446 clone.attributes = cloneAttrs; 8447 } 8448 8449 clone.value = self.value; 8450 clone.shortEnded = self.shortEnded; 8451 8452 return clone; 8453 }, 8454 8455 /** 8456 * Wraps the node in in another node. 8457 * 8458 * @example 8459 * node.wrap(wrapperNode); 8460 * 8461 * @method wrap 8462 */ 8463 wrap: function(wrapper) { 8464 var self = this; 8465 8466 self.parent.insert(wrapper, self); 8467 wrapper.append(self); 8468 8469 return self; 8470 }, 8471 8472 /** 8473 * Unwraps the node in other words it removes the node but keeps the children. 8474 * 8475 * @example 8476 * node.unwrap(); 8477 * 8478 * @method unwrap 8479 */ 8480 unwrap: function() { 8481 var self = this, node, next; 8482 8483 for (node = self.firstChild; node; ) { 8484 next = node.next; 8485 self.insert(node, self, true); 8486 node = next; 8487 } 8488 8489 self.remove(); 8490 }, 8491 8492 /** 8493 * Removes the node from it's parent. 8494 * 8495 * @example 8496 * node.remove(); 8497 * 8498 * @method remove 8499 * @return {tinymce.html.Node} Current node that got removed. 8500 */ 8501 remove: function() { 8502 var self = this, parent = self.parent, next = self.next, prev = self.prev; 8503 8504 if (parent) { 8505 if (parent.firstChild === self) { 8506 parent.firstChild = next; 8507 8508 if (next) { 8509 next.prev = null; 8510 } 8511 } else { 8512 prev.next = next; 8513 } 8514 8515 if (parent.lastChild === self) { 8516 parent.lastChild = prev; 8517 8518 if (prev) { 8519 prev.next = null; 8520 } 8521 } else { 8522 next.prev = prev; 8523 } 8524 8525 self.parent = self.next = self.prev = null; 8526 } 8527 8528 return self; 8529 }, 8530 8531 /** 8532 * Appends a new node as a child of the current node. 8533 * 8534 * @example 8535 * node.append(someNode); 8536 * 8537 * @method append 8538 * @param {tinymce.html.Node} node Node to append as a child of the current one. 8539 * @return {tinymce.html.Node} The node that got appended. 8540 */ 8541 append: function(node) { 8542 var self = this, last; 8543 8544 if (node.parent) { 8545 node.remove(); 8546 } 8547 8548 last = self.lastChild; 8549 if (last) { 8550 last.next = node; 8551 node.prev = last; 8552 self.lastChild = node; 8553 } else { 8554 self.lastChild = self.firstChild = node; 8555 } 8556 8557 node.parent = self; 8558 8559 return node; 8560 }, 8561 8562 /** 8563 * Inserts a node at a specific position as a child of the current node. 8564 * 8565 * @example 8566 * parentNode.insert(newChildNode, oldChildNode); 8567 * 8568 * @method insert 8569 * @param {tinymce.html.Node} node Node to insert as a child of the current node. 8570 * @param {tinymce.html.Node} ref_node Reference node to set node before/after. 8571 * @param {Boolean} before Optional state to insert the node before the reference node. 8572 * @return {tinymce.html.Node} The node that got inserted. 8573 */ 8574 insert: function(node, ref_node, before) { 8575 var parent; 8576 8577 if (node.parent) { 8578 node.remove(); 8579 } 8580 8581 parent = ref_node.parent || this; 8582 8583 if (before) { 8584 if (ref_node === parent.firstChild) { 8585 parent.firstChild = node; 8586 } else { 8587 ref_node.prev.next = node; 8588 } 8589 8590 node.prev = ref_node.prev; 8591 node.next = ref_node; 8592 ref_node.prev = node; 8593 } else { 8594 if (ref_node === parent.lastChild) { 8595 parent.lastChild = node; 8596 } else { 8597 ref_node.next.prev = node; 8598 } 8599 8600 node.next = ref_node.next; 8601 node.prev = ref_node; 8602 ref_node.next = node; 8603 } 8604 8605 node.parent = parent; 8606 8607 return node; 8608 }, 8609 8610 /** 8611 * Get all children by name. 8612 * 8613 * @method getAll 8614 * @param {String} name Name of the child nodes to collect. 8615 * @return {Array} Array with child nodes matchin the specified name. 8616 */ 8617 getAll: function(name) { 8618 var self = this, node, collection = []; 8619 8620 for (node = self.firstChild; node; node = walk(node, self)) { 8621 if (node.name === name) { 8622 collection.push(node); 8623 } 8624 } 8625 8626 return collection; 8627 }, 8628 8629 /** 8630 * Removes all children of the current node. 8631 * 8632 * @method empty 8633 * @return {tinymce.html.Node} The current node that got cleared. 8634 */ 8635 empty: function() { 8636 var self = this, nodes, i, node; 8637 8638 // Remove all children 8639 if (self.firstChild) { 8640 nodes = []; 8641 8642 // Collect the children 8643 for (node = self.firstChild; node; node = walk(node, self)) { 8644 nodes.push(node); 8645 } 8646 8647 // Remove the children 8648 i = nodes.length; 8649 while (i--) { 8650 node = nodes[i]; 8651 node.parent = node.firstChild = node.lastChild = node.next = node.prev = null; 8652 } 8653 } 8654 8655 self.firstChild = self.lastChild = null; 8656 8657 return self; 8658 }, 8659 8660 /** 8661 * Returns true/false if the node is to be considered empty or not. 8662 * 8663 * @example 8664 * node.isEmpty({img: true}); 8665 * @method isEmpty 8666 * @param {Object} elements Name/value object with elements that are automatically treated as non empty elements. 8667 * @return {Boolean} true/false if the node is empty or not. 8668 */ 8669 isEmpty: function(elements) { 8670 var self = this, node = self.firstChild, i, name; 8671 8672 if (node) { 8673 do { 8674 if (node.type === 1) { 8675 // Ignore bogus elements 8676 if (node.attributes.map['data-mce-bogus']) { 8677 continue; 8678 } 8679 8680 // Keep empty elements like <img /> 8681 if (elements[node.name]) { 8682 return false; 8683 } 8684 8685 // Keep elements with data attributes or name attribute like <a name="1"></a> 8686 i = node.attributes.length; 8687 while (i--) { 8688 name = node.attributes[i].name; 8689 if (name === "name" || name.indexOf('data-mce-') === 0) { 8690 return false; 8691 } 8692 } 8693 } 8694 8695 // Keep comments 8696 if (node.type === 8) { 8697 return false; 8698 } 8699 8700 // Keep non whitespace text nodes 8701 if ((node.type === 3 && !whiteSpaceRegExp.test(node.value))) { 8702 return false; 8703 } 8704 } while ((node = walk(node, self))); 8705 } 8706 8707 return true; 8708 }, 8709 8710 /** 8711 * Walks to the next or previous node and returns that node or null if it wasn't found. 8712 * 8713 * @method walk 8714 * @param {Boolean} prev Optional previous node state defaults to false. 8715 * @return {tinymce.html.Node} Node that is next to or previous of the current node. 8716 */ 8717 walk: function(prev) { 8718 return walk(this, null, prev); 8719 } 8720 }; 8721 8722 /** 8723 * Creates a node of a specific type. 8724 * 8725 * @static 8726 * @method create 8727 * @param {String} name Name of the node type to create for example "b" or "#text". 8728 * @param {Object} attrs Name/value collection of attributes that will be applied to elements. 8729 */ 8730 Node.create = function(name, attrs) { 8731 var node, attrName; 8732 8733 // Create node 8734 node = new Node(name, typeLookup[name] || 1); 8735 8736 // Add attributes if needed 8737 if (attrs) { 8738 for (attrName in attrs) { 8739 node.attr(attrName, attrs[attrName]); 8740 } 8741 } 8742 8743 return node; 8744 }; 8745 8746 return Node; 8747 }); 8748 8749 // Included from: js/tinymce/classes/html/Schema.js 8750 8751 /** 8752 * Schema.js 8753 * 8754 * Copyright, Moxiecode Systems AB 8755 * Released under LGPL License. 8756 * 8757 * License: http://www.tinymce.com/license 8758 * Contributing: http://www.tinymce.com/contributing 8759 */ 8760 8761 /** 8762 * Schema validator class. 8763 * 8764 * @class tinymce.html.Schema 8765 * @example 8766 * if (tinymce.activeEditor.schema.isValidChild('p', 'span')) 8767 * alert('span is valid child of p.'); 8768 * 8769 * if (tinymce.activeEditor.schema.getElementRule('p')) 8770 * alert('P is a valid element.'); 8771 * 8772 * @class tinymce.html.Schema 8773 * @version 3.4 8774 */ 8775 define("tinymce/html/Schema", [ 8776 "tinymce/util/Tools" 8777 ], function(Tools) { 8778 var mapCache = {}; 8779 var makeMap = Tools.makeMap, each = Tools.each, extend = Tools.extend, explode = Tools.explode, inArray = Tools.inArray; 8780 8781 function split(items, delim) { 8782 return items ? items.split(delim || ' ') : []; 8783 } 8784 8785 /** 8786 * Builds a schema lookup table 8787 * 8788 * @private 8789 * @param {String} type html4, html5 or html5-strict schema type. 8790 * @return {Object} Schema lookup table. 8791 */ 8792 function compileSchema(type) { 8793 var schema = {}, globalAttributes, blockContent; 8794 var phrasingContent, flowContent, html4BlockContent, html4PhrasingContent; 8795 8796 function add(name, attributes, children) { 8797 var ni, i, attributesOrder, args = arguments; 8798 8799 function arrayToMap(array) { 8800 var map = {}, i, l; 8801 8802 for (i = 0, l = array.length; i < l; i++) { 8803 map[array[i]] = {}; 8804 } 8805 8806 return map; 8807 } 8808 8809 children = children || []; 8810 attributes = attributes || ""; 8811 8812 if (typeof(children) === "string") { 8813 children = split(children); 8814 } 8815 8816 // Split string children 8817 for (i = 3; i < args.length; i++) { 8818 if (typeof(args[i]) === "string") { 8819 args[i] = split(args[i]); 8820 } 8821 8822 children.push.apply(children, args[i]); 8823 } 8824 8825 name = split(name); 8826 ni = name.length; 8827 while (ni--) { 8828 attributesOrder = [].concat(globalAttributes, split(attributes)); 8829 schema[name[ni]] = { 8830 attributes: arrayToMap(attributesOrder), 8831 attributesOrder: attributesOrder, 8832 children: arrayToMap(children) 8833 }; 8834 } 8835 } 8836 8837 function addAttrs(name, attributes) { 8838 var ni, schemaItem, i, l; 8839 8840 name = split(name); 8841 ni = name.length; 8842 attributes = split(attributes); 8843 while (ni--) { 8844 schemaItem = schema[name[ni]]; 8845 for (i = 0, l = attributes.length; i < l; i++) { 8846 schemaItem.attributes[attributes[i]] = {}; 8847 schemaItem.attributesOrder.push(attributes[i]); 8848 } 8849 } 8850 } 8851 8852 // Use cached schema 8853 if (mapCache[type]) { 8854 return mapCache[type]; 8855 } 8856 8857 // Attributes present on all elements 8858 globalAttributes = split("id accesskey class dir lang style tabindex title"); 8859 8860 // Event attributes can be opt-in/opt-out 8861 /*eventAttributes = split("onabort onblur oncancel oncanplay oncanplaythrough onchange onclick onclose oncontextmenu oncuechange " + 8862 "ondblclick ondrag ondragend ondragenter ondragleave ondragover ondragstart ondrop ondurationchange onemptied onended " + 8863 "onerror onfocus oninput oninvalid onkeydown onkeypress onkeyup onload onloadeddata onloadedmetadata onloadstart " + 8864 "onmousedown onmousemove onmouseout onmouseover onmouseup onmousewheel onpause onplay onplaying onprogress onratechange " + 8865 "onreset onscroll onseeked onseeking onseeking onselect onshow onstalled onsubmit onsuspend ontimeupdate onvolumechange " + 8866 "onwaiting" 8867 );*/ 8868 8869 // Block content elements 8870 blockContent = split( 8871 "address blockquote div dl fieldset form h1 h2 h3 h4 h5 h6 hr menu ol p pre table ul" 8872 ); 8873 8874 // Phrasing content elements from the HTML5 spec (inline) 8875 phrasingContent = split( 8876 "a abbr b bdo br button cite code del dfn em embed i iframe img input ins kbd " + 8877 "label map noscript object q s samp script select small span strong sub sup " + 8878 "textarea u var #text #comment" 8879 ); 8880 8881 // Add HTML5 items to globalAttributes, blockContent, phrasingContent 8882 if (type != "html4") { 8883 globalAttributes.push.apply(globalAttributes, split("contenteditable contextmenu draggable dropzone " + 8884 "hidden spellcheck translate")); 8885 blockContent.push.apply(blockContent, split("article aside details dialog figure header footer hgroup section nav")); 8886 phrasingContent.push.apply(phrasingContent, split("audio canvas command datalist mark meter output progress time wbr " + 8887 "video ruby bdi keygen")); 8888 } 8889 8890 // Add HTML4 elements unless it's html5-strict 8891 if (type != "html5-strict") { 8892 globalAttributes.push("xml:lang"); 8893 8894 html4PhrasingContent = split("acronym applet basefont big font strike tt"); 8895 phrasingContent.push.apply(phrasingContent, html4PhrasingContent); 8896 8897 each(html4PhrasingContent, function(name) { 8898 add(name, "", phrasingContent); 8899 }); 8900 8901 html4BlockContent = split("center dir isindex noframes"); 8902 blockContent.push.apply(blockContent, html4BlockContent); 8903 8904 // Flow content elements from the HTML5 spec (block+inline) 8905 flowContent = [].concat(blockContent, phrasingContent); 8906 8907 each(html4BlockContent, function(name) { 8908 add(name, "", flowContent); 8909 }); 8910 } 8911 8912 // Flow content elements from the HTML5 spec (block+inline) 8913 flowContent = flowContent || [].concat(blockContent, phrasingContent); 8914 8915 // HTML4 base schema TODO: Move HTML5 specific attributes to HTML5 specific if statement 8916 // Schema items <element name>, <specific attributes>, <children ..> 8917 add("html", "manifest", "head body"); 8918 add("head", "", "base command link meta noscript script style title"); 8919 add("title hr noscript br"); 8920 add("base", "href target"); 8921 add("link", "href rel media hreflang type sizes hreflang"); 8922 add("meta", "name http-equiv content charset"); 8923 add("style", "media type scoped"); 8924 add("script", "src async defer type charset"); 8925 add("body", "onafterprint onbeforeprint onbeforeunload onblur onerror onfocus " + 8926 "onhashchange onload onmessage onoffline ononline onpagehide onpageshow " + 8927 "onpopstate onresize onscroll onstorage onunload", flowContent); 8928 add("address dt dd div caption", "", flowContent); 8929 add("h1 h2 h3 h4 h5 h6 pre p abbr code var samp kbd sub sup i b u bdo span legend em strong small s cite dfn", "", phrasingContent); 8930 add("blockquote", "cite", flowContent); 8931 add("ol", "reversed start type", "li"); 8932 add("ul", "", "li"); 8933 add("li", "value", flowContent); 8934 add("dl", "", "dt dd"); 8935 add("a", "href target rel media hreflang type", phrasingContent); 8936 add("q", "cite", phrasingContent); 8937 add("ins del", "cite datetime", flowContent); 8938 add("img", "src alt usemap ismap width height"); 8939 add("iframe", "src name width height", flowContent); 8940 add("embed", "src type width height"); 8941 add("object", "data type typemustmatch name usemap form width height", flowContent, "param"); 8942 add("param", "name value"); 8943 add("map", "name", flowContent, "area"); 8944 add("area", "alt coords shape href target rel media hreflang type"); 8945 add("table", "border", "caption colgroup thead tfoot tbody tr" + (type == "html4" ? " col" : "")); 8946 add("colgroup", "span", "col"); 8947 add("col", "span"); 8948 add("tbody thead tfoot", "", "tr"); 8949 add("tr", "", "td th"); 8950 add("td", "colspan rowspan headers", flowContent); 8951 add("th", "colspan rowspan headers scope abbr", flowContent); 8952 add("form", "accept-charset action autocomplete enctype method name novalidate target", flowContent); 8953 add("fieldset", "disabled form name", flowContent, "legend"); 8954 add("label", "form for", phrasingContent); 8955 add("input", "accept alt autocomplete checked dirname disabled form formaction formenctype formmethod formnovalidate " + 8956 "formtarget height list max maxlength min multiple name pattern readonly required size src step type value width" 8957 ); 8958 add("button", "disabled form formaction formenctype formmethod formnovalidate formtarget name type value", 8959 type == "html4" ? flowContent : phrasingContent); 8960 add("select", "disabled form multiple name required size", "option optgroup"); 8961 add("optgroup", "disabled label", "option"); 8962 add("option", "disabled label selected value"); 8963 add("textarea", "cols dirname disabled form maxlength name readonly required rows wrap"); 8964 add("menu", "type label", flowContent, "li"); 8965 add("noscript", "", flowContent); 8966 8967 // Extend with HTML5 elements 8968 if (type != "html4") { 8969 add("wbr"); 8970 add("ruby", "", phrasingContent, "rt rp"); 8971 add("figcaption", "", flowContent); 8972 add("mark rt rp summary bdi", "", phrasingContent); 8973 add("canvas", "width height", flowContent); 8974 add("video", "src crossorigin poster preload autoplay mediagroup loop " + 8975 "muted controls width height buffered", flowContent, "track source"); 8976 add("audio", "src crossorigin preload autoplay mediagroup loop muted controls buffered volume", flowContent, "track source"); 8977 add("source", "src type media"); 8978 add("track", "kind src srclang label default"); 8979 add("datalist", "", phrasingContent, "option"); 8980 add("article section nav aside header footer", "", flowContent); 8981 add("hgroup", "", "h1 h2 h3 h4 h5 h6"); 8982 add("figure", "", flowContent, "figcaption"); 8983 add("time", "datetime", phrasingContent); 8984 add("dialog", "open", flowContent); 8985 add("command", "type label icon disabled checked radiogroup command"); 8986 add("output", "for form name", phrasingContent); 8987 add("progress", "value max", phrasingContent); 8988 add("meter", "value min max low high optimum", phrasingContent); 8989 add("details", "open", flowContent, "summary"); 8990 add("keygen", "autofocus challenge disabled form keytype name"); 8991 } 8992 8993 // Extend with HTML4 attributes unless it's html5-strict 8994 if (type != "html5-strict") { 8995 addAttrs("script", "language xml:space"); 8996 addAttrs("style", "xml:space"); 8997 addAttrs("object", "declare classid codebase codetype archive standby align border hspace vspace"); 8998 addAttrs("param", "valuetype type"); 8999 addAttrs("a", "charset name rev shape coords"); 9000 addAttrs("br", "clear"); 9001 addAttrs("applet", "codebase archive code object alt name width height align hspace vspace"); 9002 addAttrs("img", "name longdesc align border hspace vspace"); 9003 addAttrs("iframe", "longdesc frameborder marginwidth marginheight scrolling align"); 9004 addAttrs("font basefont", "size color face"); 9005 addAttrs("input", "usemap align"); 9006 addAttrs("select", "onchange"); 9007 addAttrs("textarea"); 9008 addAttrs("h1 h2 h3 h4 h5 h6 div p legend caption", "align"); 9009 addAttrs("ul", "type compact"); 9010 addAttrs("li", "type"); 9011 addAttrs("ol dl menu dir", "compact"); 9012 addAttrs("pre", "width xml:space"); 9013 addAttrs("hr", "align noshade size width"); 9014 addAttrs("isindex", "prompt"); 9015 addAttrs("table", "summary width frame rules cellspacing cellpadding align bgcolor"); 9016 addAttrs("col", "width align char charoff valign"); 9017 addAttrs("colgroup", "width align char charoff valign"); 9018 addAttrs("thead", "align char charoff valign"); 9019 addAttrs("tr", "align char charoff valign bgcolor"); 9020 addAttrs("th", "axis align char charoff valign nowrap bgcolor width height"); 9021 addAttrs("form", "accept"); 9022 addAttrs("td", "abbr axis scope align char charoff valign nowrap bgcolor width height"); 9023 addAttrs("tfoot", "align char charoff valign"); 9024 addAttrs("tbody", "align char charoff valign"); 9025 addAttrs("area", "nohref"); 9026 addAttrs("body", "background bgcolor text link vlink alink"); 9027 } 9028 9029 // Extend with HTML5 attributes unless it's html4 9030 if (type != "html4") { 9031 addAttrs("input button select textarea", "autofocus"); 9032 addAttrs("input textarea", "placeholder"); 9033 addAttrs("a", "download"); 9034 addAttrs("link script img", "crossorigin"); 9035 addAttrs("iframe", "sandbox seamless allowfullscreen"); // Excluded: srcdoc 9036 } 9037 9038 // Special: iframe, ruby, video, audio, label 9039 9040 // Delete children of the same name from it's parent 9041 // For example: form can't have a child of the name form 9042 each(split('a form meter progress dfn'), function(name) { 9043 if (schema[name]) { 9044 delete schema[name].children[name]; 9045 } 9046 }); 9047 9048 // Delete header, footer, sectioning and heading content descendants 9049 /*each('dt th address', function(name) { 9050 delete schema[name].children[name]; 9051 });*/ 9052 9053 // Caption can't have tables 9054 delete schema.caption.children.table; 9055 9056 // TODO: LI:s can only have value if parent is OL 9057 9058 // TODO: Handle transparent elements 9059 // a ins del canvas map 9060 9061 mapCache[type] = schema; 9062 9063 return schema; 9064 } 9065 9066 /** 9067 * Constructs a new Schema instance. 9068 * 9069 * @constructor 9070 * @method Schema 9071 * @param {Object} settings Name/value settings object. 9072 */ 9073 return function(settings) { 9074 var self = this, elements = {}, children = {}, patternElements = [], validStyles, schemaItems; 9075 var whiteSpaceElementsMap, selfClosingElementsMap, shortEndedElementsMap, boolAttrMap; 9076 var blockElementsMap, nonEmptyElementsMap, textBlockElementsMap, customElementsMap = {}, specialElements = {}; 9077 9078 // Creates an lookup table map object for the specified option or the default value 9079 function createLookupTable(option, default_value, extendWith) { 9080 var value = settings[option]; 9081 9082 if (!value) { 9083 // Get cached default map or make it if needed 9084 value = mapCache[option]; 9085 9086 if (!value) { 9087 value = makeMap(default_value, ' ', makeMap(default_value.toUpperCase(), ' ')); 9088 value = extend(value, extendWith); 9089 9090 mapCache[option] = value; 9091 } 9092 } else { 9093 // Create custom map 9094 value = makeMap(value, /[, ]/, makeMap(value.toUpperCase(), /[, ]/)); 9095 } 9096 9097 return value; 9098 } 9099 9100 settings = settings || {}; 9101 schemaItems = compileSchema(settings.schema); 9102 9103 // Allow all elements and attributes if verify_html is set to false 9104 if (settings.verify_html === false) { 9105 settings.valid_elements = '*[*]'; 9106 } 9107 9108 // Build styles list 9109 if (settings.valid_styles) { 9110 validStyles = {}; 9111 9112 // Convert styles into a rule list 9113 each(settings.valid_styles, function(value, key) { 9114 validStyles[key] = explode(value); 9115 }); 9116 } 9117 9118 // Setup map objects 9119 whiteSpaceElementsMap = createLookupTable('whitespace_elements', 'pre script noscript style textarea video audio iframe object'); 9120 selfClosingElementsMap = createLookupTable('self_closing_elements', 'colgroup dd dt li option p td tfoot th thead tr'); 9121 shortEndedElementsMap = createLookupTable('short_ended_elements', 'area base basefont br col frame hr img input isindex link ' + 9122 'meta param embed source wbr track'); 9123 boolAttrMap = createLookupTable('boolean_attributes', 'checked compact declare defer disabled ismap multiple nohref noresize ' + 9124 'noshade nowrap readonly selected autoplay loop controls'); 9125 nonEmptyElementsMap = createLookupTable('non_empty_elements', 'td th iframe video audio object script', shortEndedElementsMap); 9126 textBlockElementsMap = createLookupTable('text_block_elements', 'h1 h2 h3 h4 h5 h6 p div address pre form ' + 9127 'blockquote center dir fieldset header footer article section hgroup aside nav figure'); 9128 blockElementsMap = createLookupTable('block_elements', 'hr table tbody thead tfoot ' + 9129 'th tr td li ol ul caption dl dt dd noscript menu isindex option ' + 9130 'datalist select optgroup', textBlockElementsMap); 9131 9132 each((settings.special || 'script noscript style textarea').split(' '), function(name) { 9133 specialElements[name] = new RegExp('<\/' + name + '[^>]*>','gi'); 9134 }); 9135 9136 // Converts a wildcard expression string to a regexp for example *a will become /.*a/. 9137 function patternToRegExp(str) { 9138 return new RegExp('^' + str.replace(/([?+*])/g, '.$1') + '$'); 9139 } 9140 9141 // Parses the specified valid_elements string and adds to the current rules 9142 // This function is a bit hard to read since it's heavily optimized for speed 9143 function addValidElements(valid_elements) { 9144 var ei, el, ai, al, matches, element, attr, attrData, elementName, attrName, attrType, attributes, attributesOrder, 9145 prefix, outputName, globalAttributes, globalAttributesOrder, key, value, 9146 elementRuleRegExp = /^([#+\-])?([^\[!\/]+)(?:\/([^\[!]+))?(?:(!?)\[([^\]]+)\])?$/, 9147 attrRuleRegExp = /^([!\-])?(\w+::\w+|[^=:<]+)?(?:([=:<])(.*))?$/, 9148 hasPatternsRegExp = /[*?+]/; 9149 9150 if (valid_elements) { 9151 // Split valid elements into an array with rules 9152 valid_elements = split(valid_elements, ','); 9153 9154 if (elements['@']) { 9155 globalAttributes = elements['@'].attributes; 9156 globalAttributesOrder = elements['@'].attributesOrder; 9157 } 9158 9159 // Loop all rules 9160 for (ei = 0, el = valid_elements.length; ei < el; ei++) { 9161 // Parse element rule 9162 matches = elementRuleRegExp.exec(valid_elements[ei]); 9163 if (matches) { 9164 // Setup local names for matches 9165 prefix = matches[1]; 9166 elementName = matches[2]; 9167 outputName = matches[3]; 9168 attrData = matches[5]; 9169 9170 // Create new attributes and attributesOrder 9171 attributes = {}; 9172 attributesOrder = []; 9173 9174 // Create the new element 9175 element = { 9176 attributes: attributes, 9177 attributesOrder: attributesOrder 9178 }; 9179 9180 // Padd empty elements prefix 9181 if (prefix === '#') { 9182 element.paddEmpty = true; 9183 } 9184 9185 // Remove empty elements prefix 9186 if (prefix === '-') { 9187 element.removeEmpty = true; 9188 } 9189 9190 if (matches[4] === '!') { 9191 element.removeEmptyAttrs = true; 9192 } 9193 9194 // Copy attributes from global rule into current rule 9195 if (globalAttributes) { 9196 for (key in globalAttributes) { 9197 attributes[key] = globalAttributes[key]; 9198 } 9199 9200 attributesOrder.push.apply(attributesOrder, globalAttributesOrder); 9201 } 9202 9203 // Attributes defined 9204 if (attrData) { 9205 attrData = split(attrData, '|'); 9206 for (ai = 0, al = attrData.length; ai < al; ai++) { 9207 matches = attrRuleRegExp.exec(attrData[ai]); 9208 if (matches) { 9209 attr = {}; 9210 attrType = matches[1]; 9211 attrName = matches[2].replace(/::/g, ':'); 9212 prefix = matches[3]; 9213 value = matches[4]; 9214 9215 // Required 9216 if (attrType === '!') { 9217 element.attributesRequired = element.attributesRequired || []; 9218 element.attributesRequired.push(attrName); 9219 attr.required = true; 9220 } 9221 9222 // Denied from global 9223 if (attrType === '-') { 9224 delete attributes[attrName]; 9225 attributesOrder.splice(inArray(attributesOrder, attrName), 1); 9226 continue; 9227 } 9228 9229 // Default value 9230 if (prefix) { 9231 // Default value 9232 if (prefix === '=') { 9233 element.attributesDefault = element.attributesDefault || []; 9234 element.attributesDefault.push({name: attrName, value: value}); 9235 attr.defaultValue = value; 9236 } 9237 9238 // Forced value 9239 if (prefix === ':') { 9240 element.attributesForced = element.attributesForced || []; 9241 element.attributesForced.push({name: attrName, value: value}); 9242 attr.forcedValue = value; 9243 } 9244 9245 // Required values 9246 if (prefix === '<') { 9247 attr.validValues = makeMap(value, '?'); 9248 } 9249 } 9250 9251 // Check for attribute patterns 9252 if (hasPatternsRegExp.test(attrName)) { 9253 element.attributePatterns = element.attributePatterns || []; 9254 attr.pattern = patternToRegExp(attrName); 9255 element.attributePatterns.push(attr); 9256 } else { 9257 // Add attribute to order list if it doesn't already exist 9258 if (!attributes[attrName]) { 9259 attributesOrder.push(attrName); 9260 } 9261 9262 attributes[attrName] = attr; 9263 } 9264 } 9265 } 9266 } 9267 9268 // Global rule, store away these for later usage 9269 if (!globalAttributes && elementName == '@') { 9270 globalAttributes = attributes; 9271 globalAttributesOrder = attributesOrder; 9272 } 9273 9274 // Handle substitute elements such as b/strong 9275 if (outputName) { 9276 element.outputName = elementName; 9277 elements[outputName] = element; 9278 } 9279 9280 // Add pattern or exact element 9281 if (hasPatternsRegExp.test(elementName)) { 9282 element.pattern = patternToRegExp(elementName); 9283 patternElements.push(element); 9284 } else { 9285 elements[elementName] = element; 9286 } 9287 } 9288 } 9289 } 9290 } 9291 9292 function setValidElements(valid_elements) { 9293 elements = {}; 9294 patternElements = []; 9295 9296 addValidElements(valid_elements); 9297 9298 each(schemaItems, function(element, name) { 9299 children[name] = element.children; 9300 }); 9301 } 9302 9303 // Adds custom non HTML elements to the schema 9304 function addCustomElements(custom_elements) { 9305 var customElementRegExp = /^(~)?(.+)$/; 9306 9307 if (custom_elements) { 9308 // Flush cached items since we are altering the default maps 9309 mapCache.text_block_elements = mapCache.block_elements = null; 9310 9311 each(split(custom_elements, ','), function(rule) { 9312 var matches = customElementRegExp.exec(rule), 9313 inline = matches[1] === '~', 9314 cloneName = inline ? 'span' : 'div', 9315 name = matches[2]; 9316 9317 children[name] = children[cloneName]; 9318 customElementsMap[name] = cloneName; 9319 9320 // If it's not marked as inline then add it to valid block elements 9321 if (!inline) { 9322 blockElementsMap[name.toUpperCase()] = {}; 9323 blockElementsMap[name] = {}; 9324 } 9325 9326 // Add elements clone if needed 9327 if (!elements[name]) { 9328 var customRule = elements[cloneName]; 9329 9330 customRule = extend({}, customRule); 9331 delete customRule.removeEmptyAttrs; 9332 delete customRule.removeEmpty; 9333 9334 elements[name] = customRule; 9335 } 9336 9337 // Add custom elements at span/div positions 9338 each(children, function(element, elmName) { 9339 if (element[cloneName]) { 9340 children[elmName] = element = extend({}, children[elmName]); 9341 element[name] = element[cloneName]; 9342 } 9343 }); 9344 }); 9345 } 9346 } 9347 9348 // Adds valid children to the schema object 9349 function addValidChildren(valid_children) { 9350 var childRuleRegExp = /^([+\-]?)(\w+)\[([^\]]+)\]$/; 9351 9352 if (valid_children) { 9353 each(split(valid_children, ','), function(rule) { 9354 var matches = childRuleRegExp.exec(rule), parent, prefix; 9355 9356 if (matches) { 9357 prefix = matches[1]; 9358 9359 // Add/remove items from default 9360 if (prefix) { 9361 parent = children[matches[2]]; 9362 } else { 9363 parent = children[matches[2]] = {'#comment': {}}; 9364 } 9365 9366 parent = children[matches[2]]; 9367 9368 each(split(matches[3], '|'), function(child) { 9369 if (prefix === '-') { 9370 // Clone the element before we delete 9371 // things in it to not mess up default schemas 9372 children[matches[2]] = parent = extend({}, children[matches[2]]); 9373 9374 delete parent[child]; 9375 } else { 9376 parent[child] = {}; 9377 } 9378 }); 9379 } 9380 }); 9381 } 9382 } 9383 9384 function getElementRule(name) { 9385 var element = elements[name], i; 9386 9387 // Exact match found 9388 if (element) { 9389 return element; 9390 } 9391 9392 // No exact match then try the patterns 9393 i = patternElements.length; 9394 while (i--) { 9395 element = patternElements[i]; 9396 9397 if (element.pattern.test(name)) { 9398 return element; 9399 } 9400 } 9401 } 9402 9403 if (!settings.valid_elements) { 9404 // No valid elements defined then clone the elements from the schema spec 9405 each(schemaItems, function(element, name) { 9406 elements[name] = { 9407 attributes: element.attributes, 9408 attributesOrder: element.attributesOrder 9409 }; 9410 9411 children[name] = element.children; 9412 }); 9413 9414 // Switch these on HTML4 9415 if (settings.schema != "html5") { 9416 each(split('strong/b em/i'), function(item) { 9417 item = split(item, '/'); 9418 elements[item[1]].outputName = item[0]; 9419 }); 9420 } 9421 9422 // Add default alt attribute for images 9423 elements.img.attributesDefault = [{name: 'alt', value: ''}]; 9424 9425 // Remove these if they are empty by default 9426 each(split('ol ul sub sup blockquote span font a table tbody tr strong em b i'), function(name) { 9427 if (elements[name]) { 9428 elements[name].removeEmpty = true; 9429 } 9430 }); 9431 9432 // Padd these by default 9433 each(split('p h1 h2 h3 h4 h5 h6 th td pre div address caption'), function(name) { 9434 elements[name].paddEmpty = true; 9435 }); 9436 9437 // Remove these if they have no attributes 9438 each(split('span'), function(name) { 9439 elements[name].removeEmptyAttrs = true; 9440 }); 9441 9442 // Remove these by default 9443 // TODO: Reenable in 4.1 9444 /*each(split('script style'), function(name) { 9445 delete elements[name]; 9446 });*/ 9447 } else { 9448 setValidElements(settings.valid_elements); 9449 } 9450 9451 addCustomElements(settings.custom_elements); 9452 addValidChildren(settings.valid_children); 9453 addValidElements(settings.extended_valid_elements); 9454 9455 // Todo: Remove this when we fix list handling to be valid 9456 addValidChildren('+ol[ul|ol],+ul[ul|ol]'); 9457 9458 // Delete invalid elements 9459 if (settings.invalid_elements) { 9460 each(explode(settings.invalid_elements), function(item) { 9461 if (elements[item]) { 9462 delete elements[item]; 9463 } 9464 }); 9465 } 9466 9467 // If the user didn't allow span only allow internal spans 9468 if (!getElementRule('span')) { 9469 addValidElements('span[!data-mce-type|*]'); 9470 } 9471 9472 /** 9473 * Name/value map object with valid parents and children to those parents. 9474 * 9475 * @example 9476 * children = { 9477 * div:{p:{}, h1:{}} 9478 * }; 9479 * @field children 9480 * @type Object 9481 */ 9482 self.children = children; 9483 9484 /** 9485 * Name/value map object with valid styles for each element. 9486 * 9487 * @field styles 9488 * @type Object 9489 */ 9490 self.styles = validStyles; 9491 9492 /** 9493 * Returns a map with boolean attributes. 9494 * 9495 * @method getBoolAttrs 9496 * @return {Object} Name/value lookup map for boolean attributes. 9497 */ 9498 self.getBoolAttrs = function() { 9499 return boolAttrMap; 9500 }; 9501 9502 /** 9503 * Returns a map with block elements. 9504 * 9505 * @method getBlockElements 9506 * @return {Object} Name/value lookup map for block elements. 9507 */ 9508 self.getBlockElements = function() { 9509 return blockElementsMap; 9510 }; 9511 9512 /** 9513 * Returns a map with text block elements. Such as: p,h1-h6,div,address 9514 * 9515 * @method getTextBlockElements 9516 * @return {Object} Name/value lookup map for block elements. 9517 */ 9518 self.getTextBlockElements = function() { 9519 return textBlockElementsMap; 9520 }; 9521 9522 /** 9523 * Returns a map with short ended elements such as BR or IMG. 9524 * 9525 * @method getShortEndedElements 9526 * @return {Object} Name/value lookup map for short ended elements. 9527 */ 9528 self.getShortEndedElements = function() { 9529 return shortEndedElementsMap; 9530 }; 9531 9532 /** 9533 * Returns a map with self closing tags such as <li>. 9534 * 9535 * @method getSelfClosingElements 9536 * @return {Object} Name/value lookup map for self closing tags elements. 9537 */ 9538 self.getSelfClosingElements = function() { 9539 return selfClosingElementsMap; 9540 }; 9541 9542 /** 9543 * Returns a map with elements that should be treated as contents regardless if it has text 9544 * content in them or not such as TD, VIDEO or IMG. 9545 * 9546 * @method getNonEmptyElements 9547 * @return {Object} Name/value lookup map for non empty elements. 9548 */ 9549 self.getNonEmptyElements = function() { 9550 return nonEmptyElementsMap; 9551 }; 9552 9553 /** 9554 * Returns a map with elements where white space is to be preserved like PRE or SCRIPT. 9555 * 9556 * @method getWhiteSpaceElements 9557 * @return {Object} Name/value lookup map for white space elements. 9558 */ 9559 self.getWhiteSpaceElements = function() { 9560 return whiteSpaceElementsMap; 9561 }; 9562 9563 /** 9564 * Returns a map with special elements. These are elements that needs to be parsed 9565 * in a special way such as script, style, textarea etc. The map object values 9566 * are regexps used to find the end of the element. 9567 * 9568 * @method getSpecialElements 9569 * @return {Object} Name/value lookup map for special elements. 9570 */ 9571 self.getSpecialElements = function() { 9572 return specialElements; 9573 }; 9574 9575 /** 9576 * Returns true/false if the specified element and it's child is valid or not 9577 * according to the schema. 9578 * 9579 * @method isValidChild 9580 * @param {String} name Element name to check for. 9581 * @param {String} child Element child to verify. 9582 * @return {Boolean} True/false if the element is a valid child of the specified parent. 9583 */ 9584 self.isValidChild = function(name, child) { 9585 var parent = children[name]; 9586 9587 return !!(parent && parent[child]); 9588 }; 9589 9590 /** 9591 * Returns true/false if the specified element name and optional attribute is 9592 * valid according to the schema. 9593 * 9594 * @method isValid 9595 * @param {String} name Name of element to check. 9596 * @param {String} attr Optional attribute name to check for. 9597 * @return {Boolean} True/false if the element and attribute is valid. 9598 */ 9599 self.isValid = function(name, attr) { 9600 var attrPatterns, i, rule = getElementRule(name); 9601 9602 // Check if it's a valid element 9603 if (rule) { 9604 if (attr) { 9605 // Check if attribute name exists 9606 if (rule.attributes[attr]) { 9607 return true; 9608 } 9609 9610 // Check if attribute matches a regexp pattern 9611 attrPatterns = rule.attributePatterns; 9612 if (attrPatterns) { 9613 i = attrPatterns.length; 9614 while (i--) { 9615 if (attrPatterns[i].pattern.test(name)) { 9616 return true; 9617 } 9618 } 9619 } 9620 } else { 9621 return true; 9622 } 9623 } 9624 9625 // No match 9626 return false; 9627 }; 9628 9629 /** 9630 * Returns true/false if the specified element is valid or not 9631 * according to the schema. 9632 * 9633 * @method getElementRule 9634 * @param {String} name Element name to check for. 9635 * @return {Object} Element object or undefined if the element isn't valid. 9636 */ 9637 self.getElementRule = getElementRule; 9638 9639 /** 9640 * Returns an map object of all custom elements. 9641 * 9642 * @method getCustomElements 9643 * @return {Object} Name/value map object of all custom elements. 9644 */ 9645 self.getCustomElements = function() { 9646 return customElementsMap; 9647 }; 9648 9649 /** 9650 * Parses a valid elements string and adds it to the schema. The valid elements 9651 * format is for example "element[attr=default|otherattr]". 9652 * Existing rules will be replaced with the ones specified, so this extends the schema. 9653 * 9654 * @method addValidElements 9655 * @param {String} valid_elements String in the valid elements format to be parsed. 9656 */ 9657 self.addValidElements = addValidElements; 9658 9659 /** 9660 * Parses a valid elements string and sets it to the schema. The valid elements 9661 * format is for example "element[attr=default|otherattr]". 9662 * Existing rules will be replaced with the ones specified, so this extends the schema. 9663 * 9664 * @method setValidElements 9665 * @param {String} valid_elements String in the valid elements format to be parsed. 9666 */ 9667 self.setValidElements = setValidElements; 9668 9669 /** 9670 * Adds custom non HTML elements to the schema. 9671 * 9672 * @method addCustomElements 9673 * @param {String} custom_elements Comma separated list of custom elements to add. 9674 */ 9675 self.addCustomElements = addCustomElements; 9676 9677 /** 9678 * Parses a valid children string and adds them to the schema structure. The valid children 9679 * format is for example: "element[child1|child2]". 9680 * 9681 * @method addValidChildren 9682 * @param {String} valid_children Valid children elements string to parse 9683 */ 9684 self.addValidChildren = addValidChildren; 9685 9686 self.elements = elements; 9687 }; 9688 }); 9689 9690 // Included from: js/tinymce/classes/html/SaxParser.js 9691 9692 /** 9693 * SaxParser.js 9694 * 9695 * Copyright, Moxiecode Systems AB 9696 * Released under LGPL License. 9697 * 9698 * License: http://www.tinymce.com/license 9699 * Contributing: http://www.tinymce.com/contributing 9700 */ 9701 9702 /*eslint max-depth:[2, 9] */ 9703 9704 /** 9705 * This class parses HTML code using pure JavaScript and executes various events for each item it finds. It will 9706 * always execute the events in the right order for tag soup code like <b><p></b></p>. It will also remove elements 9707 * and attributes that doesn't fit the schema if the validate setting is enabled. 9708 * 9709 * @example 9710 * var parser = new tinymce.html.SaxParser({ 9711 * validate: true, 9712 * 9713 * comment: function(text) { 9714 * console.log('Comment:', text); 9715 * }, 9716 * 9717 * cdata: function(text) { 9718 * console.log('CDATA:', text); 9719 * }, 9720 * 9721 * text: function(text, raw) { 9722 * console.log('Text:', text, 'Raw:', raw); 9723 * }, 9724 * 9725 * start: function(name, attrs, empty) { 9726 * console.log('Start:', name, attrs, empty); 9727 * }, 9728 * 9729 * end: function(name) { 9730 * console.log('End:', name); 9731 * }, 9732 * 9733 * pi: function(name, text) { 9734 * console.log('PI:', name, text); 9735 * }, 9736 * 9737 * doctype: function(text) { 9738 * console.log('DocType:', text); 9739 * } 9740 * }, schema); 9741 * @class tinymce.html.SaxParser 9742 * @version 3.4 9743 */ 9744 define("tinymce/html/SaxParser", [ 9745 "tinymce/html/Schema", 9746 "tinymce/html/Entities", 9747 "tinymce/util/Tools" 9748 ], function(Schema, Entities, Tools) { 9749 var each = Tools.each; 9750 9751 /** 9752 * Constructs a new SaxParser instance. 9753 * 9754 * @constructor 9755 * @method SaxParser 9756 * @param {Object} settings Name/value collection of settings. comment, cdata, text, start and end are callbacks. 9757 * @param {tinymce.html.Schema} schema HTML Schema class to use when parsing. 9758 */ 9759 return function(settings, schema) { 9760 var self = this; 9761 9762 function noop() {} 9763 9764 settings = settings || {}; 9765 self.schema = schema = schema || new Schema(); 9766 9767 if (settings.fix_self_closing !== false) { 9768 settings.fix_self_closing = true; 9769 } 9770 9771 // Add handler functions from settings and setup default handlers 9772 each('comment cdata text start end pi doctype'.split(' '), function(name) { 9773 if (name) { 9774 self[name] = settings[name] || noop; 9775 } 9776 }); 9777 9778 /** 9779 * Parses the specified HTML string and executes the callbacks for each item it finds. 9780 * 9781 * @example 9782 * new SaxParser({...}).parse('<b>text</b>'); 9783 * @method parse 9784 * @param {String} html Html string to sax parse. 9785 */ 9786 self.parse = function(html) { 9787 var self = this, matches, index = 0, value, endRegExp, stack = [], attrList, i, text, name; 9788 var isInternalElement, removeInternalElements, shortEndedElements, fillAttrsMap, isShortEnded; 9789 var validate, elementRule, isValidElement, attr, attribsValue, validAttributesMap, validAttributePatterns; 9790 var attributesRequired, attributesDefault, attributesForced; 9791 var anyAttributesRequired, selfClosing, tokenRegExp, attrRegExp, specialElements, attrValue, idCount = 0; 9792 var decode = Entities.decode, fixSelfClosing, filteredUrlAttrs = Tools.makeMap('src,href,data,background,formaction,poster'); 9793 var scriptUriRegExp = /((java|vb)script|mhtml):/i, dataUriRegExp = /^data:/i; 9794 9795 function processEndTag(name) { 9796 var pos, i; 9797 9798 // Find position of parent of the same type 9799 pos = stack.length; 9800 while (pos--) { 9801 if (stack[pos].name === name) { 9802 break; 9803 } 9804 } 9805 9806 // Found parent 9807 if (pos >= 0) { 9808 // Close all the open elements 9809 for (i = stack.length - 1; i >= pos; i--) { 9810 name = stack[i]; 9811 9812 if (name.valid) { 9813 self.end(name.name); 9814 } 9815 } 9816 9817 // Remove the open elements from the stack 9818 stack.length = pos; 9819 } 9820 } 9821 9822 function parseAttribute(match, name, value, val2, val3) { 9823 var attrRule, i, trimRegExp = /[\s\u0000-\u001F]+/g; 9824 9825 name = name.toLowerCase(); 9826 value = name in fillAttrsMap ? name : decode(value || val2 || val3 || ''); // Handle boolean attribute than value attribute 9827 9828 // Validate name and value pass through all data- attributes 9829 if (validate && !isInternalElement && name.indexOf('data-') !== 0) { 9830 attrRule = validAttributesMap[name]; 9831 9832 // Find rule by pattern matching 9833 if (!attrRule && validAttributePatterns) { 9834 i = validAttributePatterns.length; 9835 while (i--) { 9836 attrRule = validAttributePatterns[i]; 9837 if (attrRule.pattern.test(name)) { 9838 break; 9839 } 9840 } 9841 9842 // No rule matched 9843 if (i === -1) { 9844 attrRule = null; 9845 } 9846 } 9847 9848 // No attribute rule found 9849 if (!attrRule) { 9850 return; 9851 } 9852 9853 // Validate value 9854 if (attrRule.validValues && !(value in attrRule.validValues)) { 9855 return; 9856 } 9857 } 9858 9859 // Block any javascript: urls or non image data uris 9860 if (filteredUrlAttrs[name] && !settings.allow_script_urls) { 9861 var uri = value.replace(trimRegExp, ''); 9862 9863 try { 9864 // Might throw malformed URI sequence 9865 uri = decodeURIComponent(uri); 9866 } catch (ex) { 9867 // Fallback to non UTF-8 decoder 9868 uri = unescape(uri); 9869 } 9870 9871 if (scriptUriRegExp.test(uri)) { 9872 return; 9873 } 9874 9875 if (!settings.allow_html_data_urls && dataUriRegExp.test(uri) && !/^data:image\//i.test(uri)) { 9876 return; 9877 } 9878 } 9879 9880 // Add attribute to list and map 9881 attrList.map[name] = value; 9882 attrList.push({ 9883 name: name, 9884 value: value 9885 }); 9886 } 9887 9888 // Precompile RegExps and map objects 9889 tokenRegExp = new RegExp('<(?:' + 9890 '(?:!--([\\w\\W]*?)-->)|' + // Comment 9891 '(?:!\\[CDATA\\[([\\w\\W]*?)\\]\\]>)|' + // CDATA 9892 '(?:!DOCTYPE([\\w\\W]*?)>)|' + // DOCTYPE 9893 '(?:\\?([^\\s\\/<>]+) ?([\\w\\W]*?)[?/]>)|' + // PI 9894 '(?:\\/([^>]+)>)|' + // End element 9895 '(?:([A-Za-z0-9\\-\\:\\.]+)((?:\\s+[^"\'>]+(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>]*))*|\\/|\\s+)>)' + // Start element 9896 ')', 'g'); 9897 9898 attrRegExp = /([\w:\-]+)(?:\s*=\s*(?:(?:\"((?:[^\"])*)\")|(?:\'((?:[^\'])*)\')|([^>\s]+)))?/g; 9899 9900 // Setup lookup tables for empty elements and boolean attributes 9901 shortEndedElements = schema.getShortEndedElements(); 9902 selfClosing = settings.self_closing_elements || schema.getSelfClosingElements(); 9903 fillAttrsMap = schema.getBoolAttrs(); 9904 validate = settings.validate; 9905 removeInternalElements = settings.remove_internals; 9906 fixSelfClosing = settings.fix_self_closing; 9907 specialElements = schema.getSpecialElements(); 9908 9909 while ((matches = tokenRegExp.exec(html))) { 9910 // Text 9911 if (index < matches.index) { 9912 self.text(decode(html.substr(index, matches.index - index))); 9913 } 9914 9915 if ((value = matches[6])) { // End element 9916 value = value.toLowerCase(); 9917 9918 // IE will add a ":" in front of elements it doesn't understand like custom elements or HTML5 elements 9919 if (value.charAt(0) === ':') { 9920 value = value.substr(1); 9921 } 9922 9923 processEndTag(value); 9924 } else if ((value = matches[7])) { // Start element 9925 value = value.toLowerCase(); 9926 9927 // IE will add a ":" in front of elements it doesn't understand like custom elements or HTML5 elements 9928 if (value.charAt(0) === ':') { 9929 value = value.substr(1); 9930 } 9931 9932 isShortEnded = value in shortEndedElements; 9933 9934 // Is self closing tag for example an <li> after an open <li> 9935 if (fixSelfClosing && selfClosing[value] && stack.length > 0 && stack[stack.length - 1].name === value) { 9936 processEndTag(value); 9937 } 9938 9939 // Validate element 9940 if (!validate || (elementRule = schema.getElementRule(value))) { 9941 isValidElement = true; 9942 9943 // Grab attributes map and patters when validation is enabled 9944 if (validate) { 9945 validAttributesMap = elementRule.attributes; 9946 validAttributePatterns = elementRule.attributePatterns; 9947 } 9948 9949 // Parse attributes 9950 if ((attribsValue = matches[8])) { 9951 isInternalElement = attribsValue.indexOf('data-mce-type') !== -1; // Check if the element is an internal element 9952 9953 // If the element has internal attributes then remove it if we are told to do so 9954 if (isInternalElement && removeInternalElements) { 9955 isValidElement = false; 9956 } 9957 9958 attrList = []; 9959 attrList.map = {}; 9960 9961 attribsValue.replace(attrRegExp, parseAttribute); 9962 } else { 9963 attrList = []; 9964 attrList.map = {}; 9965 } 9966 9967 // Process attributes if validation is enabled 9968 if (validate && !isInternalElement) { 9969 attributesRequired = elementRule.attributesRequired; 9970 attributesDefault = elementRule.attributesDefault; 9971 attributesForced = elementRule.attributesForced; 9972 anyAttributesRequired = elementRule.removeEmptyAttrs; 9973 9974 // Check if any attribute exists 9975 if (anyAttributesRequired && !attrList.length) { 9976 isValidElement = false; 9977 } 9978 9979 // Handle forced attributes 9980 if (attributesForced) { 9981 i = attributesForced.length; 9982 while (i--) { 9983 attr = attributesForced[i]; 9984 name = attr.name; 9985 attrValue = attr.value; 9986 9987 if (attrValue === '{$uid}') { 9988 attrValue = 'mce_' + idCount++; 9989 } 9990 9991 attrList.map[name] = attrValue; 9992 attrList.push({name: name, value: attrValue}); 9993 } 9994 } 9995 9996 // Handle default attributes 9997 if (attributesDefault) { 9998 i = attributesDefault.length; 9999 while (i--) { 10000 attr = attributesDefault[i]; 10001 name = attr.name; 10002 10003 if (!(name in attrList.map)) { 10004 attrValue = attr.value; 10005 10006 if (attrValue === '{$uid}') { 10007 attrValue = 'mce_' + idCount++; 10008 } 10009 10010 attrList.map[name] = attrValue; 10011 attrList.push({name: name, value: attrValue}); 10012 } 10013 } 10014 } 10015 10016 // Handle required attributes 10017 if (attributesRequired) { 10018 i = attributesRequired.length; 10019 while (i--) { 10020 if (attributesRequired[i] in attrList.map) { 10021 break; 10022 } 10023 } 10024 10025 // None of the required attributes where found 10026 if (i === -1) { 10027 isValidElement = false; 10028 } 10029 } 10030 10031 // Invalidate element if it's marked as bogus 10032 if (attrList.map['data-mce-bogus']) { 10033 isValidElement = false; 10034 } 10035 } 10036 10037 if (isValidElement) { 10038 self.start(value, attrList, isShortEnded); 10039 } 10040 } else { 10041 isValidElement = false; 10042 } 10043 10044 // Treat script, noscript and style a bit different since they may include code that looks like elements 10045 if ((endRegExp = specialElements[value])) { 10046 endRegExp.lastIndex = index = matches.index + matches[0].length; 10047 10048 if ((matches = endRegExp.exec(html))) { 10049 if (isValidElement) { 10050 text = html.substr(index, matches.index - index); 10051 } 10052 10053 index = matches.index + matches[0].length; 10054 } else { 10055 text = html.substr(index); 10056 index = html.length; 10057 } 10058 10059 if (isValidElement) { 10060 if (text.length > 0) { 10061 self.text(text, true); 10062 } 10063 10064 self.end(value); 10065 } 10066 10067 tokenRegExp.lastIndex = index; 10068 continue; 10069 } 10070 10071 // Push value on to stack 10072 if (!isShortEnded) { 10073 if (!attribsValue || attribsValue.indexOf('/') != attribsValue.length - 1) { 10074 stack.push({name: value, valid: isValidElement}); 10075 } else if (isValidElement) { 10076 self.end(value); 10077 } 10078 } 10079 } else if ((value = matches[1])) { // Comment 10080 // Padd comment value to avoid browsers from parsing invalid comments as HTML 10081 if (value.charAt(0) === '>') { 10082 value = ' ' + value; 10083 } 10084 10085 if (!settings.allow_conditional_comments && value.substr(0, 3) === '[if') { 10086 value = ' ' + value; 10087 } 10088 10089 self.comment(value); 10090 } else if ((value = matches[2])) { // CDATA 10091 self.cdata(value); 10092 } else if ((value = matches[3])) { // DOCTYPE 10093 self.doctype(value); 10094 } else if ((value = matches[4])) { // PI 10095 self.pi(value, matches[5]); 10096 } 10097 10098 index = matches.index + matches[0].length; 10099 } 10100 10101 // Text 10102 if (index < html.length) { 10103 self.text(decode(html.substr(index))); 10104 } 10105 10106 // Close any open elements 10107 for (i = stack.length - 1; i >= 0; i--) { 10108 value = stack[i]; 10109 10110 if (value.valid) { 10111 self.end(value.name); 10112 } 10113 } 10114 }; 10115 }; 10116 }); 10117 10118 // Included from: js/tinymce/classes/html/DomParser.js 10119 10120 /** 10121 * DomParser.js 10122 * 10123 * Copyright, Moxiecode Systems AB 10124 * Released under LGPL License. 10125 * 10126 * License: http://www.tinymce.com/license 10127 * Contributing: http://www.tinymce.com/contributing 10128 */ 10129 10130 /** 10131 * This class parses HTML code into a DOM like structure of nodes it will remove redundant whitespace and make 10132 * sure that the node tree is valid according to the specified schema. 10133 * So for example: <p>a<p>b</p>c</p> will become <p>a</p><p>b</p><p>c</p> 10134 * 10135 * @example 10136 * var parser = new tinymce.html.DomParser({validate: true}, schema); 10137 * var rootNode = parser.parse('<h1>content</h1>'); 10138 * 10139 * @class tinymce.html.DomParser 10140 * @version 3.4 10141 */ 10142 define("tinymce/html/DomParser", [ 10143 "tinymce/html/Node", 10144 "tinymce/html/Schema", 10145 "tinymce/html/SaxParser", 10146 "tinymce/util/Tools" 10147 ], function(Node, Schema, SaxParser, Tools) { 10148 var makeMap = Tools.makeMap, each = Tools.each, explode = Tools.explode, extend = Tools.extend; 10149 10150 /** 10151 * Constructs a new DomParser instance. 10152 * 10153 * @constructor 10154 * @method DomParser 10155 * @param {Object} settings Name/value collection of settings. comment, cdata, text, start and end are callbacks. 10156 * @param {tinymce.html.Schema} schema HTML Schema class to use when parsing. 10157 */ 10158 return function(settings, schema) { 10159 var self = this, nodeFilters = {}, attributeFilters = [], matchedNodes = {}, matchedAttributes = {}; 10160 10161 settings = settings || {}; 10162 settings.validate = "validate" in settings ? settings.validate : true; 10163 settings.root_name = settings.root_name || 'body'; 10164 self.schema = schema = schema || new Schema(); 10165 10166 function fixInvalidChildren(nodes) { 10167 var ni, node, parent, parents, newParent, currentNode, tempNode, childNode, i; 10168 var nonEmptyElements, nonSplitableElements, textBlockElements, sibling, nextNode; 10169 10170 nonSplitableElements = makeMap('tr,td,th,tbody,thead,tfoot,table'); 10171 nonEmptyElements = schema.getNonEmptyElements(); 10172 textBlockElements = schema.getTextBlockElements(); 10173 10174 for (ni = 0; ni < nodes.length; ni++) { 10175 node = nodes[ni]; 10176 10177 // Already removed or fixed 10178 if (!node.parent || node.fixed) { 10179 continue; 10180 } 10181 10182 // If the invalid element is a text block and the text block is within a parent LI element 10183 // Then unwrap the first text block and convert other sibling text blocks to LI elements similar to Word/Open Office 10184 if (textBlockElements[node.name] && node.parent.name == 'li') { 10185 // Move sibling text blocks after LI element 10186 sibling = node.next; 10187 while (sibling) { 10188 if (textBlockElements[sibling.name]) { 10189 sibling.name = 'li'; 10190 sibling.fixed = true; 10191 node.parent.insert(sibling, node.parent); 10192 } else { 10193 break; 10194 } 10195 10196 sibling = sibling.next; 10197 } 10198 10199 // Unwrap current text block 10200 node.unwrap(node); 10201 continue; 10202 } 10203 10204 // Get list of all parent nodes until we find a valid parent to stick the child into 10205 parents = [node]; 10206 for (parent = node.parent; parent && !schema.isValidChild(parent.name, node.name) && 10207 !nonSplitableElements[parent.name]; parent = parent.parent) { 10208 parents.push(parent); 10209 } 10210 10211 // Found a suitable parent 10212 if (parent && parents.length > 1) { 10213 // Reverse the array since it makes looping easier 10214 parents.reverse(); 10215 10216 // Clone the related parent and insert that after the moved node 10217 newParent = currentNode = self.filterNode(parents[0].clone()); 10218 10219 // Start cloning and moving children on the left side of the target node 10220 for (i = 0; i < parents.length - 1; i++) { 10221 if (schema.isValidChild(currentNode.name, parents[i].name)) { 10222 tempNode = self.filterNode(parents[i].clone()); 10223 currentNode.append(tempNode); 10224 } else { 10225 tempNode = currentNode; 10226 } 10227 10228 for (childNode = parents[i].firstChild; childNode && childNode != parents[i + 1]; ) { 10229 nextNode = childNode.next; 10230 tempNode.append(childNode); 10231 childNode = nextNode; 10232 } 10233 10234 currentNode = tempNode; 10235 } 10236 10237 if (!newParent.isEmpty(nonEmptyElements)) { 10238 parent.insert(newParent, parents[0], true); 10239 parent.insert(node, newParent); 10240 } else { 10241 parent.insert(node, parents[0], true); 10242 } 10243 10244 // Check if the element is empty by looking through it's contents and special treatment for <p><br /></p> 10245 parent = parents[0]; 10246 if (parent.isEmpty(nonEmptyElements) || parent.firstChild === parent.lastChild && parent.firstChild.name === 'br') { 10247 parent.empty().remove(); 10248 } 10249 } else if (node.parent) { 10250 // If it's an LI try to find a UL/OL for it or wrap it 10251 if (node.name === 'li') { 10252 sibling = node.prev; 10253 if (sibling && (sibling.name === 'ul' || sibling.name === 'ul')) { 10254 sibling.append(node); 10255 continue; 10256 } 10257 10258 sibling = node.next; 10259 if (sibling && (sibling.name === 'ul' || sibling.name === 'ul')) { 10260 sibling.insert(node, sibling.firstChild, true); 10261 continue; 10262 } 10263 10264 node.wrap(self.filterNode(new Node('ul', 1))); 10265 continue; 10266 } 10267 10268 // Try wrapping the element in a DIV 10269 if (schema.isValidChild(node.parent.name, 'div') && schema.isValidChild('div', node.name)) { 10270 node.wrap(self.filterNode(new Node('div', 1))); 10271 } else { 10272 // We failed wrapping it, then remove or unwrap it 10273 if (node.name === 'style' || node.name === 'script') { 10274 node.empty().remove(); 10275 } else { 10276 node.unwrap(); 10277 } 10278 } 10279 } 10280 } 10281 } 10282 10283 /** 10284 * Runs the specified node though the element and attributes filters. 10285 * 10286 * @method filterNode 10287 * @param {tinymce.html.Node} Node the node to run filters on. 10288 * @return {tinymce.html.Node} The passed in node. 10289 */ 10290 self.filterNode = function(node) { 10291 var i, name, list; 10292 10293 // Run element filters 10294 if (name in nodeFilters) { 10295 list = matchedNodes[name]; 10296 10297 if (list) { 10298 list.push(node); 10299 } else { 10300 matchedNodes[name] = [node]; 10301 } 10302 } 10303 10304 // Run attribute filters 10305 i = attributeFilters.length; 10306 while (i--) { 10307 name = attributeFilters[i].name; 10308 10309 if (name in node.attributes.map) { 10310 list = matchedAttributes[name]; 10311 10312 if (list) { 10313 list.push(node); 10314 } else { 10315 matchedAttributes[name] = [node]; 10316 } 10317 } 10318 } 10319 10320 return node; 10321 }; 10322 10323 /** 10324 * Adds a node filter function to the parser, the parser will collect the specified nodes by name 10325 * and then execute the callback ones it has finished parsing the document. 10326 * 10327 * @example 10328 * parser.addNodeFilter('p,h1', function(nodes, name) { 10329 * for (var i = 0; i < nodes.length; i++) { 10330 * console.log(nodes[i].name); 10331 * } 10332 * }); 10333 * @method addNodeFilter 10334 * @method {String} name Comma separated list of nodes to collect. 10335 * @param {function} callback Callback function to execute once it has collected nodes. 10336 */ 10337 self.addNodeFilter = function(name, callback) { 10338 each(explode(name), function(name) { 10339 var list = nodeFilters[name]; 10340 10341 if (!list) { 10342 nodeFilters[name] = list = []; 10343 } 10344 10345 list.push(callback); 10346 }); 10347 }; 10348 10349 /** 10350 * Adds a attribute filter function to the parser, the parser will collect nodes that has the specified attributes 10351 * and then execute the callback ones it has finished parsing the document. 10352 * 10353 * @example 10354 * parser.addAttributeFilter('src,href', function(nodes, name) { 10355 * for (var i = 0; i < nodes.length; i++) { 10356 * console.log(nodes[i].name); 10357 * } 10358 * }); 10359 * @method addAttributeFilter 10360 * @method {String} name Comma separated list of nodes to collect. 10361 * @param {function} callback Callback function to execute once it has collected nodes. 10362 */ 10363 self.addAttributeFilter = function(name, callback) { 10364 each(explode(name), function(name) { 10365 var i; 10366 10367 for (i = 0; i < attributeFilters.length; i++) { 10368 if (attributeFilters[i].name === name) { 10369 attributeFilters[i].callbacks.push(callback); 10370 return; 10371 } 10372 } 10373 10374 attributeFilters.push({name: name, callbacks: [callback]}); 10375 }); 10376 }; 10377 10378 /** 10379 * Parses the specified HTML string into a DOM like node tree and returns the result. 10380 * 10381 * @example 10382 * var rootNode = new DomParser({...}).parse('<b>text</b>'); 10383 * @method parse 10384 * @param {String} html Html string to sax parse. 10385 * @param {Object} args Optional args object that gets passed to all filter functions. 10386 * @return {tinymce.html.Node} Root node containing the tree. 10387 */ 10388 self.parse = function(html, args) { 10389 var parser, rootNode, node, nodes, i, l, fi, fl, list, name, validate; 10390 var blockElements, startWhiteSpaceRegExp, invalidChildren = [], isInWhiteSpacePreservedElement; 10391 var endWhiteSpaceRegExp, allWhiteSpaceRegExp, isAllWhiteSpaceRegExp, whiteSpaceElements; 10392 var children, nonEmptyElements, rootBlockName; 10393 10394 args = args || {}; 10395 matchedNodes = {}; 10396 matchedAttributes = {}; 10397 blockElements = extend(makeMap('script,style,head,html,body,title,meta,param'), schema.getBlockElements()); 10398 nonEmptyElements = schema.getNonEmptyElements(); 10399 children = schema.children; 10400 validate = settings.validate; 10401 rootBlockName = "forced_root_block" in args ? args.forced_root_block : settings.forced_root_block; 10402 10403 whiteSpaceElements = schema.getWhiteSpaceElements(); 10404 startWhiteSpaceRegExp = /^[ \t\r\n]+/; 10405 endWhiteSpaceRegExp = /[ \t\r\n]+$/; 10406 allWhiteSpaceRegExp = /[ \t\r\n]+/g; 10407 isAllWhiteSpaceRegExp = /^[ \t\r\n]+$/; 10408 10409 function addRootBlocks() { 10410 var node = rootNode.firstChild, next, rootBlockNode; 10411 10412 // Removes whitespace at beginning and end of block so: 10413 // <p> x </p> -> <p>x</p> 10414 function trim(rootBlockNode) { 10415 if (rootBlockNode) { 10416 node = rootBlockNode.firstChild; 10417 if (node && node.type == 3) { 10418 node.value = node.value.replace(startWhiteSpaceRegExp, ''); 10419 } 10420 10421 node = rootBlockNode.lastChild; 10422 if (node && node.type == 3) { 10423 node.value = node.value.replace(endWhiteSpaceRegExp, ''); 10424 } 10425 } 10426 } 10427 10428 // Check if rootBlock is valid within rootNode for example if P is valid in H1 if H1 is the contentEditabe root 10429 if (!schema.isValidChild(rootNode.name, rootBlockName.toLowerCase())) { 10430 return; 10431 } 10432 10433 while (node) { 10434 next = node.next; 10435 10436 if (node.type == 3 || (node.type == 1 && node.name !== 'p' && 10437 !blockElements[node.name] && !node.attr('data-mce-type'))) { 10438 if (!rootBlockNode) { 10439 // Create a new root block element 10440 rootBlockNode = createNode(rootBlockName, 1); 10441 rootBlockNode.attr(settings.forced_root_block_attrs); 10442 rootNode.insert(rootBlockNode, node); 10443 rootBlockNode.append(node); 10444 } else { 10445 rootBlockNode.append(node); 10446 } 10447 } else { 10448 trim(rootBlockNode); 10449 rootBlockNode = null; 10450 } 10451 10452 node = next; 10453 } 10454 10455 trim(rootBlockNode); 10456 } 10457 10458 function createNode(name, type) { 10459 var node = new Node(name, type), list; 10460 10461 if (name in nodeFilters) { 10462 list = matchedNodes[name]; 10463 10464 if (list) { 10465 list.push(node); 10466 } else { 10467 matchedNodes[name] = [node]; 10468 } 10469 } 10470 10471 return node; 10472 } 10473 10474 function removeWhitespaceBefore(node) { 10475 var textNode, textVal, sibling; 10476 10477 for (textNode = node.prev; textNode && textNode.type === 3; ) { 10478 textVal = textNode.value.replace(endWhiteSpaceRegExp, ''); 10479 10480 if (textVal.length > 0) { 10481 textNode.value = textVal; 10482 textNode = textNode.prev; 10483 } else { 10484 sibling = textNode.prev; 10485 textNode.remove(); 10486 textNode = sibling; 10487 } 10488 } 10489 } 10490 10491 function cloneAndExcludeBlocks(input) { 10492 var name, output = {}; 10493 10494 for (name in input) { 10495 if (name !== 'li' && name != 'p') { 10496 output[name] = input[name]; 10497 } 10498 } 10499 10500 return output; 10501 } 10502 10503 parser = new SaxParser({ 10504 validate: validate, 10505 allow_script_urls: settings.allow_script_urls, 10506 allow_conditional_comments: settings.allow_conditional_comments, 10507 10508 // Exclude P and LI from DOM parsing since it's treated better by the DOM parser 10509 self_closing_elements: cloneAndExcludeBlocks(schema.getSelfClosingElements()), 10510 10511 cdata: function(text) { 10512 node.append(createNode('#cdata', 4)).value = text; 10513 }, 10514 10515 text: function(text, raw) { 10516 var textNode; 10517 10518 // Trim all redundant whitespace on non white space elements 10519 if (!isInWhiteSpacePreservedElement) { 10520 text = text.replace(allWhiteSpaceRegExp, ' '); 10521 10522 if (node.lastChild && blockElements[node.lastChild.name]) { 10523 text = text.replace(startWhiteSpaceRegExp, ''); 10524 } 10525 } 10526 10527 // Do we need to create the node 10528 if (text.length !== 0) { 10529 textNode = createNode('#text', 3); 10530 textNode.raw = !!raw; 10531 node.append(textNode).value = text; 10532 } 10533 }, 10534 10535 comment: function(text) { 10536 node.append(createNode('#comment', 8)).value = text; 10537 }, 10538 10539 pi: function(name, text) { 10540 node.append(createNode(name, 7)).value = text; 10541 removeWhitespaceBefore(node); 10542 }, 10543 10544 doctype: function(text) { 10545 var newNode; 10546 10547 newNode = node.append(createNode('#doctype', 10)); 10548 newNode.value = text; 10549 removeWhitespaceBefore(node); 10550 }, 10551 10552 start: function(name, attrs, empty) { 10553 var newNode, attrFiltersLen, elementRule, attrName, parent; 10554 10555 elementRule = validate ? schema.getElementRule(name) : {}; 10556 if (elementRule) { 10557 newNode = createNode(elementRule.outputName || name, 1); 10558 newNode.attributes = attrs; 10559 newNode.shortEnded = empty; 10560 10561 node.append(newNode); 10562 10563 // Check if node is valid child of the parent node is the child is 10564 // unknown we don't collect it since it's probably a custom element 10565 parent = children[node.name]; 10566 if (parent && children[newNode.name] && !parent[newNode.name]) { 10567 invalidChildren.push(newNode); 10568 } 10569 10570 attrFiltersLen = attributeFilters.length; 10571 while (attrFiltersLen--) { 10572 attrName = attributeFilters[attrFiltersLen].name; 10573 10574 if (attrName in attrs.map) { 10575 list = matchedAttributes[attrName]; 10576 10577 if (list) { 10578 list.push(newNode); 10579 } else { 10580 matchedAttributes[attrName] = [newNode]; 10581 } 10582 } 10583 } 10584 10585 // Trim whitespace before block 10586 if (blockElements[name]) { 10587 removeWhitespaceBefore(newNode); 10588 } 10589 10590 // Change current node if the element wasn't empty i.e not <br /> or <img /> 10591 if (!empty) { 10592 node = newNode; 10593 } 10594 10595 // Check if we are inside a whitespace preserved element 10596 if (!isInWhiteSpacePreservedElement && whiteSpaceElements[name]) { 10597 isInWhiteSpacePreservedElement = true; 10598 } 10599 } 10600 }, 10601 10602 end: function(name) { 10603 var textNode, elementRule, text, sibling, tempNode; 10604 10605 elementRule = validate ? schema.getElementRule(name) : {}; 10606 if (elementRule) { 10607 if (blockElements[name]) { 10608 if (!isInWhiteSpacePreservedElement) { 10609 // Trim whitespace of the first node in a block 10610 textNode = node.firstChild; 10611 if (textNode && textNode.type === 3) { 10612 text = textNode.value.replace(startWhiteSpaceRegExp, ''); 10613 10614 // Any characters left after trim or should we remove it 10615 if (text.length > 0) { 10616 textNode.value = text; 10617 textNode = textNode.next; 10618 } else { 10619 sibling = textNode.next; 10620 textNode.remove(); 10621 textNode = sibling; 10622 10623 // Remove any pure whitespace siblings 10624 while (textNode && textNode.type === 3) { 10625 text = textNode.value; 10626 sibling = textNode.next; 10627 10628 if (text.length === 0 || isAllWhiteSpaceRegExp.test(text)) { 10629 textNode.remove(); 10630 textNode = sibling; 10631 } 10632 10633 textNode = sibling; 10634 } 10635 } 10636 } 10637 10638 // Trim whitespace of the last node in a block 10639 textNode = node.lastChild; 10640 if (textNode && textNode.type === 3) { 10641 text = textNode.value.replace(endWhiteSpaceRegExp, ''); 10642 10643 // Any characters left after trim or should we remove it 10644 if (text.length > 0) { 10645 textNode.value = text; 10646 textNode = textNode.prev; 10647 } else { 10648 sibling = textNode.prev; 10649 textNode.remove(); 10650 textNode = sibling; 10651 10652 // Remove any pure whitespace siblings 10653 while (textNode && textNode.type === 3) { 10654 text = textNode.value; 10655 sibling = textNode.prev; 10656 10657 if (text.length === 0 || isAllWhiteSpaceRegExp.test(text)) { 10658 textNode.remove(); 10659 textNode = sibling; 10660 } 10661 10662 textNode = sibling; 10663 } 10664 } 10665 } 10666 } 10667 10668 // Trim start white space 10669 // Removed due to: #5424 10670 /*textNode = node.prev; 10671 if (textNode && textNode.type === 3) { 10672 text = textNode.value.replace(startWhiteSpaceRegExp, ''); 10673 10674 if (text.length > 0) 10675 textNode.value = text; 10676 else 10677 textNode.remove(); 10678 }*/ 10679 } 10680 10681 // Check if we exited a whitespace preserved element 10682 if (isInWhiteSpacePreservedElement && whiteSpaceElements[name]) { 10683 isInWhiteSpacePreservedElement = false; 10684 } 10685 10686 // Handle empty nodes 10687 if (elementRule.removeEmpty || elementRule.paddEmpty) { 10688 if (node.isEmpty(nonEmptyElements)) { 10689 if (elementRule.paddEmpty) { 10690 node.empty().append(new Node('#text', '3')).value = '\u00a0'; 10691 } else { 10692 // Leave nodes that have a name like <a name="name"> 10693 if (!node.attributes.map.name && !node.attributes.map.id) { 10694 tempNode = node.parent; 10695 node.empty().remove(); 10696 node = tempNode; 10697 return; 10698 } 10699 } 10700 } 10701 } 10702 10703 node = node.parent; 10704 } 10705 } 10706 }, schema); 10707 10708 rootNode = node = new Node(args.context || settings.root_name, 11); 10709 10710 parser.parse(html); 10711 10712 // Fix invalid children or report invalid children in a contextual parsing 10713 if (validate && invalidChildren.length) { 10714 if (!args.context) { 10715 fixInvalidChildren(invalidChildren); 10716 } else { 10717 args.invalid = true; 10718 } 10719 } 10720 10721 // Wrap nodes in the root into block elements if the root is body 10722 if (rootBlockName && (rootNode.name == 'body' || args.isRootContent)) { 10723 addRootBlocks(); 10724 } 10725 10726 // Run filters only when the contents is valid 10727 if (!args.invalid) { 10728 // Run node filters 10729 for (name in matchedNodes) { 10730 list = nodeFilters[name]; 10731 nodes = matchedNodes[name]; 10732 10733 // Remove already removed children 10734 fi = nodes.length; 10735 while (fi--) { 10736 if (!nodes[fi].parent) { 10737 nodes.splice(fi, 1); 10738 } 10739 } 10740 10741 for (i = 0, l = list.length; i < l; i++) { 10742 list[i](nodes, name, args); 10743 } 10744 } 10745 10746 // Run attribute filters 10747 for (i = 0, l = attributeFilters.length; i < l; i++) { 10748 list = attributeFilters[i]; 10749 10750 if (list.name in matchedAttributes) { 10751 nodes = matchedAttributes[list.name]; 10752 10753 // Remove already removed children 10754 fi = nodes.length; 10755 while (fi--) { 10756 if (!nodes[fi].parent) { 10757 nodes.splice(fi, 1); 10758 } 10759 } 10760 10761 for (fi = 0, fl = list.callbacks.length; fi < fl; fi++) { 10762 list.callbacks[fi](nodes, list.name, args); 10763 } 10764 } 10765 } 10766 } 10767 10768 return rootNode; 10769 }; 10770 10771 // Remove <br> at end of block elements Gecko and WebKit injects BR elements to 10772 // make it possible to place the caret inside empty blocks. This logic tries to remove 10773 // these elements and keep br elements that where intended to be there intact 10774 if (settings.remove_trailing_brs) { 10775 self.addNodeFilter('br', function(nodes) { 10776 var i, l = nodes.length, node, blockElements = extend({}, schema.getBlockElements()); 10777 var nonEmptyElements = schema.getNonEmptyElements(), parent, lastParent, prev, prevName; 10778 var elementRule, textNode; 10779 10780 // Remove brs from body element as well 10781 blockElements.body = 1; 10782 10783 // Must loop forwards since it will otherwise remove all brs in <p>a<br><br><br></p> 10784 for (i = 0; i < l; i++) { 10785 node = nodes[i]; 10786 parent = node.parent; 10787 10788 if (blockElements[node.parent.name] && node === parent.lastChild) { 10789 // Loop all nodes to the left of the current node and check for other BR elements 10790 // excluding bookmarks since they are invisible 10791 prev = node.prev; 10792 while (prev) { 10793 prevName = prev.name; 10794 10795 // Ignore bookmarks 10796 if (prevName !== "span" || prev.attr('data-mce-type') !== 'bookmark') { 10797 // Found a non BR element 10798 if (prevName !== "br") { 10799 break; 10800 } 10801 10802 // Found another br it's a <br><br> structure then don't remove anything 10803 if (prevName === 'br') { 10804 node = null; 10805 break; 10806 } 10807 } 10808 10809 prev = prev.prev; 10810 } 10811 10812 if (node) { 10813 node.remove(); 10814 10815 // Is the parent to be considered empty after we removed the BR 10816 if (parent.isEmpty(nonEmptyElements)) { 10817 elementRule = schema.getElementRule(parent.name); 10818 10819 // Remove or padd the element depending on schema rule 10820 if (elementRule) { 10821 if (elementRule.removeEmpty) { 10822 parent.remove(); 10823 } else if (elementRule.paddEmpty) { 10824 parent.empty().append(new Node('#text', 3)).value = '\u00a0'; 10825 } 10826 } 10827 } 10828 } 10829 } else { 10830 // Replaces BR elements inside inline elements like <p><b><i><br></i></b></p> 10831 // so they become <p><b><i> </i></b></p> 10832 lastParent = node; 10833 while (parent && parent.firstChild === lastParent && parent.lastChild === lastParent) { 10834 lastParent = parent; 10835 10836 if (blockElements[parent.name]) { 10837 break; 10838 } 10839 10840 parent = parent.parent; 10841 } 10842 10843 if (lastParent === parent) { 10844 textNode = new Node('#text', 3); 10845 textNode.value = '\u00a0'; 10846 node.replace(textNode); 10847 } 10848 } 10849 } 10850 }); 10851 } 10852 10853 // Force anchor names closed, unless the setting "allow_html_in_named_anchor" is explicitly included. 10854 if (!settings.allow_html_in_named_anchor) { 10855 self.addAttributeFilter('id,name', function(nodes) { 10856 var i = nodes.length, sibling, prevSibling, parent, node; 10857 10858 while (i--) { 10859 node = nodes[i]; 10860 if (node.name === 'a' && node.firstChild && !node.attr('href')) { 10861 parent = node.parent; 10862 10863 // Move children after current node 10864 sibling = node.lastChild; 10865 do { 10866 prevSibling = sibling.prev; 10867 parent.insert(sibling, node); 10868 sibling = prevSibling; 10869 } while (sibling); 10870 } 10871 } 10872 }); 10873 } 10874 }; 10875 }); 10876 10877 // Included from: js/tinymce/classes/html/Writer.js 10878 10879 /** 10880 * Writer.js 10881 * 10882 * Copyright, Moxiecode Systems AB 10883 * Released under LGPL License. 10884 * 10885 * License: http://www.tinymce.com/license 10886 * Contributing: http://www.tinymce.com/contributing 10887 */ 10888 10889 /** 10890 * This class is used to write HTML tags out it can be used with the Serializer or the SaxParser. 10891 * 10892 * @class tinymce.html.Writer 10893 * @example 10894 * var writer = new tinymce.html.Writer({indent: true}); 10895 * var parser = new tinymce.html.SaxParser(writer).parse('<p><br></p>'); 10896 * console.log(writer.getContent()); 10897 * 10898 * @class tinymce.html.Writer 10899 * @version 3.4 10900 */ 10901 define("tinymce/html/Writer", [ 10902 "tinymce/html/Entities", 10903 "tinymce/util/Tools" 10904 ], function(Entities, Tools) { 10905 var makeMap = Tools.makeMap; 10906 10907 /** 10908 * Constructs a new Writer instance. 10909 * 10910 * @constructor 10911 * @method Writer 10912 * @param {Object} settings Name/value settings object. 10913 */ 10914 return function(settings) { 10915 var html = [], indent, indentBefore, indentAfter, encode, htmlOutput; 10916 10917 settings = settings || {}; 10918 indent = settings.indent; 10919 indentBefore = makeMap(settings.indent_before || ''); 10920 indentAfter = makeMap(settings.indent_after || ''); 10921 encode = Entities.getEncodeFunc(settings.entity_encoding || 'raw', settings.entities); 10922 htmlOutput = settings.element_format == "html"; 10923 10924 return { 10925 /** 10926 * Writes the a start element such as <p id="a">. 10927 * 10928 * @method start 10929 * @param {String} name Name of the element. 10930 * @param {Array} attrs Optional attribute array or undefined if it hasn't any. 10931 * @param {Boolean} empty Optional empty state if the tag should end like <br />. 10932 */ 10933 start: function(name, attrs, empty) { 10934 var i, l, attr, value; 10935 10936 if (indent && indentBefore[name] && html.length > 0) { 10937 value = html[html.length - 1]; 10938 10939 if (value.length > 0 && value !== '\n') { 10940 html.push('\n'); 10941 } 10942 } 10943 10944 html.push('<', name); 10945 10946 if (attrs) { 10947 for (i = 0, l = attrs.length; i < l; i++) { 10948 attr = attrs[i]; 10949 html.push(' ', attr.name, '="', encode(attr.value, true), '"'); 10950 } 10951 } 10952 10953 if (!empty || htmlOutput) { 10954 html[html.length] = '>'; 10955 } else { 10956 html[html.length] = ' />'; 10957 } 10958 10959 if (empty && indent && indentAfter[name] && html.length > 0) { 10960 value = html[html.length - 1]; 10961 10962 if (value.length > 0 && value !== '\n') { 10963 html.push('\n'); 10964 } 10965 } 10966 }, 10967 10968 /** 10969 * Writes the a end element such as </p>. 10970 * 10971 * @method end 10972 * @param {String} name Name of the element. 10973 */ 10974 end: function(name) { 10975 var value; 10976 10977 /*if (indent && indentBefore[name] && html.length > 0) { 10978 value = html[html.length - 1]; 10979 10980 if (value.length > 0 && value !== '\n') 10981 html.push('\n'); 10982 }*/ 10983 10984 html.push('</', name, '>'); 10985 10986 if (indent && indentAfter[name] && html.length > 0) { 10987 value = html[html.length - 1]; 10988 10989 if (value.length > 0 && value !== '\n') { 10990 html.push('\n'); 10991 } 10992 } 10993 }, 10994 10995 /** 10996 * Writes a text node. 10997 * 10998 * @method text 10999 * @param {String} text String to write out. 11000 * @param {Boolean} raw Optional raw state if true the contents wont get encoded. 11001 */ 11002 text: function(text, raw) { 11003 if (text.length > 0) { 11004 html[html.length] = raw ? text : encode(text); 11005 } 11006 }, 11007 11008 /** 11009 * Writes a cdata node such as <![CDATA[data]]>. 11010 * 11011 * @method cdata 11012 * @param {String} text String to write out inside the cdata. 11013 */ 11014 cdata: function(text) { 11015 html.push('<![CDATA[', text, ']]>'); 11016 }, 11017 11018 /** 11019 * Writes a comment node such as <!-- Comment -->. 11020 * 11021 * @method cdata 11022 * @param {String} text String to write out inside the comment. 11023 */ 11024 comment: function(text) { 11025 html.push('<!--', text, '-->'); 11026 }, 11027 11028 /** 11029 * Writes a PI node such as <?xml attr="value" ?>. 11030 * 11031 * @method pi 11032 * @param {String} name Name of the pi. 11033 * @param {String} text String to write out inside the pi. 11034 */ 11035 pi: function(name, text) { 11036 if (text) { 11037 html.push('<?', name, ' ', text, '?>'); 11038 } else { 11039 html.push('<?', name, '?>'); 11040 } 11041 11042 if (indent) { 11043 html.push('\n'); 11044 } 11045 }, 11046 11047 /** 11048 * Writes a doctype node such as <!DOCTYPE data>. 11049 * 11050 * @method doctype 11051 * @param {String} text String to write out inside the doctype. 11052 */ 11053 doctype: function(text) { 11054 html.push('<!DOCTYPE', text, '>', indent ? '\n' : ''); 11055 }, 11056 11057 /** 11058 * Resets the internal buffer if one wants to reuse the writer. 11059 * 11060 * @method reset 11061 */ 11062 reset: function() { 11063 html.length = 0; 11064 }, 11065 11066 /** 11067 * Returns the contents that got serialized. 11068 * 11069 * @method getContent 11070 * @return {String} HTML contents that got written down. 11071 */ 11072 getContent: function() { 11073 return html.join('').replace(/\n$/, ''); 11074 } 11075 }; 11076 }; 11077 }); 11078 11079 // Included from: js/tinymce/classes/html/Serializer.js 11080 11081 /** 11082 * Serializer.js 11083 * 11084 * Copyright, Moxiecode Systems AB 11085 * Released under LGPL License. 11086 * 11087 * License: http://www.tinymce.com/license 11088 * Contributing: http://www.tinymce.com/contributing 11089 */ 11090 11091 /** 11092 * This class is used to serialize down the DOM tree into a string using a Writer instance. 11093 * 11094 * 11095 * @example 11096 * new tinymce.html.Serializer().serialize(new tinymce.html.DomParser().parse('<p>text</p>')); 11097 * @class tinymce.html.Serializer 11098 * @version 3.4 11099 */ 11100 define("tinymce/html/Serializer", [ 11101 "tinymce/html/Writer", 11102 "tinymce/html/Schema" 11103 ], function(Writer, Schema) { 11104 /** 11105 * Constructs a new Serializer instance. 11106 * 11107 * @constructor 11108 * @method Serializer 11109 * @param {Object} settings Name/value settings object. 11110 * @param {tinymce.html.Schema} schema Schema instance to use. 11111 */ 11112 return function(settings, schema) { 11113 var self = this, writer = new Writer(settings); 11114 11115 settings = settings || {}; 11116 settings.validate = "validate" in settings ? settings.validate : true; 11117 11118 self.schema = schema = schema || new Schema(); 11119 self.writer = writer; 11120 11121 /** 11122 * Serializes the specified node into a string. 11123 * 11124 * @example 11125 * new tinymce.html.Serializer().serialize(new tinymce.html.DomParser().parse('<p>text</p>')); 11126 * @method serialize 11127 * @param {tinymce.html.Node} node Node instance to serialize. 11128 * @return {String} String with HTML based on DOM tree. 11129 */ 11130 self.serialize = function(node) { 11131 var handlers, validate; 11132 11133 validate = settings.validate; 11134 11135 handlers = { 11136 // #text 11137 3: function(node) { 11138 writer.text(node.value, node.raw); 11139 }, 11140 11141 // #comment 11142 8: function(node) { 11143 writer.comment(node.value); 11144 }, 11145 11146 // Processing instruction 11147 7: function(node) { 11148 writer.pi(node.name, node.value); 11149 }, 11150 11151 // Doctype 11152 10: function(node) { 11153 writer.doctype(node.value); 11154 }, 11155 11156 // CDATA 11157 4: function(node) { 11158 writer.cdata(node.value); 11159 }, 11160 11161 // Document fragment 11162 11: function(node) { 11163 if ((node = node.firstChild)) { 11164 do { 11165 walk(node); 11166 } while ((node = node.next)); 11167 } 11168 } 11169 }; 11170 11171 writer.reset(); 11172 11173 function walk(node) { 11174 var handler = handlers[node.type], name, isEmpty, attrs, attrName, attrValue, sortedAttrs, i, l, elementRule; 11175 11176 if (!handler) { 11177 name = node.name; 11178 isEmpty = node.shortEnded; 11179 attrs = node.attributes; 11180 11181 // Sort attributes 11182 if (validate && attrs && attrs.length > 1) { 11183 sortedAttrs = []; 11184 sortedAttrs.map = {}; 11185 11186 elementRule = schema.getElementRule(node.name); 11187 for (i = 0, l = elementRule.attributesOrder.length; i < l; i++) { 11188 attrName = elementRule.attributesOrder[i]; 11189 11190 if (attrName in attrs.map) { 11191 attrValue = attrs.map[attrName]; 11192 sortedAttrs.map[attrName] = attrValue; 11193 sortedAttrs.push({name: attrName, value: attrValue}); 11194 } 11195 } 11196 11197 for (i = 0, l = attrs.length; i < l; i++) { 11198 attrName = attrs[i].name; 11199 11200 if (!(attrName in sortedAttrs.map)) { 11201 attrValue = attrs.map[attrName]; 11202 sortedAttrs.map[attrName] = attrValue; 11203 sortedAttrs.push({name: attrName, value: attrValue}); 11204 } 11205 } 11206 11207 attrs = sortedAttrs; 11208 } 11209 11210 writer.start(node.name, attrs, isEmpty); 11211 11212 if (!isEmpty) { 11213 if ((node = node.firstChild)) { 11214 do { 11215 walk(node); 11216 } while ((node = node.next)); 11217 } 11218 11219 writer.end(name); 11220 } 11221 } else { 11222 handler(node); 11223 } 11224 } 11225 11226 // Serialize element and treat all non elements as fragments 11227 if (node.type == 1 && !settings.inner) { 11228 walk(node); 11229 } else { 11230 handlers[11](node); 11231 } 11232 11233 return writer.getContent(); 11234 }; 11235 }; 11236 }); 11237 11238 // Included from: js/tinymce/classes/dom/Serializer.js 11239 11240 /** 11241 * Serializer.js 11242 * 11243 * Copyright, Moxiecode Systems AB 11244 * Released under LGPL License. 11245 * 11246 * License: http://www.tinymce.com/license 11247 * Contributing: http://www.tinymce.com/contributing 11248 */ 11249 11250 /** 11251 * This class is used to serialize DOM trees into a string. Consult the TinyMCE Wiki API for 11252 * more details and examples on how to use this class. 11253 * 11254 * @class tinymce.dom.Serializer 11255 */ 11256 define("tinymce/dom/Serializer", [ 11257 "tinymce/dom/DOMUtils", 11258 "tinymce/html/DomParser", 11259 "tinymce/html/Entities", 11260 "tinymce/html/Serializer", 11261 "tinymce/html/Node", 11262 "tinymce/html/Schema", 11263 "tinymce/Env", 11264 "tinymce/util/Tools" 11265 ], function(DOMUtils, DomParser, Entities, Serializer, Node, Schema, Env, Tools) { 11266 var each = Tools.each, trim = Tools.trim; 11267 var DOM = DOMUtils.DOM; 11268 11269 /** 11270 * Constructs a new DOM serializer class. 11271 * 11272 * @constructor 11273 * @method Serializer 11274 * @param {Object} settings Serializer settings object. 11275 * @param {tinymce.Editor} editor Optional editor to bind events to and get schema/dom from. 11276 */ 11277 return function(settings, editor) { 11278 var dom, schema, htmlParser; 11279 11280 if (editor) { 11281 dom = editor.dom; 11282 schema = editor.schema; 11283 } 11284 11285 // Default DOM and Schema if they are undefined 11286 dom = dom || DOM; 11287 schema = schema || new Schema(settings); 11288 settings.entity_encoding = settings.entity_encoding || 'named'; 11289 settings.remove_trailing_brs = "remove_trailing_brs" in settings ? settings.remove_trailing_brs : true; 11290 11291 htmlParser = new DomParser(settings, schema); 11292 11293 // Convert tabindex back to elements when serializing contents 11294 htmlParser.addAttributeFilter('data-mce-tabindex', function(nodes, name) { 11295 var i = nodes.length, node; 11296 11297 while (i--) { 11298 node = nodes[i]; 11299 node.attr('tabindex', node.attributes.map['data-mce-tabindex']); 11300 node.attr(name, null); 11301 } 11302 }); 11303 11304 // Convert move data-mce-src, data-mce-href and data-mce-style into nodes or process them if needed 11305 htmlParser.addAttributeFilter('src,href,style', function(nodes, name) { 11306 var i = nodes.length, node, value, internalName = 'data-mce-' + name; 11307 var urlConverter = settings.url_converter, urlConverterScope = settings.url_converter_scope, undef; 11308 11309 while (i--) { 11310 node = nodes[i]; 11311 11312 value = node.attributes.map[internalName]; 11313 if (value !== undef) { 11314 // Set external name to internal value and remove internal 11315 node.attr(name, value.length > 0 ? value : null); 11316 node.attr(internalName, null); 11317 } else { 11318 // No internal attribute found then convert the value we have in the DOM 11319 value = node.attributes.map[name]; 11320 11321 if (name === "style") { 11322 value = dom.serializeStyle(dom.parseStyle(value), node.name); 11323 } else if (urlConverter) { 11324 value = urlConverter.call(urlConverterScope, value, name, node.name); 11325 } 11326 11327 node.attr(name, value.length > 0 ? value : null); 11328 } 11329 } 11330 }); 11331 11332 // Remove internal classes mceItem<..> or mceSelected 11333 htmlParser.addAttributeFilter('class', function(nodes) { 11334 var i = nodes.length, node, value; 11335 11336 while (i--) { 11337 node = nodes[i]; 11338 value = node.attr('class').replace(/(?:^|\s)mce-item-\w+(?!\S)/g, ''); 11339 node.attr('class', value.length > 0 ? value : null); 11340 } 11341 }); 11342 11343 // Remove bookmark elements 11344 htmlParser.addAttributeFilter('data-mce-type', function(nodes, name, args) { 11345 var i = nodes.length, node; 11346 11347 while (i--) { 11348 node = nodes[i]; 11349 11350 if (node.attributes.map['data-mce-type'] === 'bookmark' && !args.cleanup) { 11351 node.remove(); 11352 } 11353 } 11354 }); 11355 11356 // Remove expando attributes 11357 htmlParser.addAttributeFilter('data-mce-expando', function(nodes, name) { 11358 var i = nodes.length; 11359 11360 while (i--) { 11361 nodes[i].attr(name, null); 11362 } 11363 }); 11364 11365 htmlParser.addNodeFilter('noscript', function(nodes) { 11366 var i = nodes.length, node; 11367 11368 while (i--) { 11369 node = nodes[i].firstChild; 11370 11371 if (node) { 11372 node.value = Entities.decode(node.value); 11373 } 11374 } 11375 }); 11376 11377 // Force script into CDATA sections and remove the mce- prefix also add comments around styles 11378 htmlParser.addNodeFilter('script,style', function(nodes, name) { 11379 var i = nodes.length, node, value; 11380 11381 function trim(value) { 11382 /*jshint maxlen:255 */ 11383 /*eslint max-len:0 */ 11384 return value.replace(/(<!--\[CDATA\[|\]\]-->)/g, '\n') 11385 .replace(/^[\r\n]*|[\r\n]*$/g, '') 11386 .replace(/^\s*((<!--)?(\s*\/\/)?\s*<!\[CDATA\[|(<!--\s*)?\/\*\s*<!\[CDATA\[\s*\*\/|(\/\/)?\s*<!--|\/\*\s*<!--\s*\*\/)\s*[\r\n]*/gi, '') 11387 .replace(/\s*(\/\*\s*\]\]>\s*\*\/(-->)?|\s*\/\/\s*\]\]>(-->)?|\/\/\s*(-->)?|\]\]>|\/\*\s*-->\s*\*\/|\s*-->\s*)\s*$/g, ''); 11388 } 11389 11390 while (i--) { 11391 node = nodes[i]; 11392 value = node.firstChild ? node.firstChild.value : ''; 11393 11394 if (name === "script") { 11395 // Remove mce- prefix from script elements and remove default text/javascript mime type (HTML5) 11396 var type = (node.attr('type') || 'text/javascript').replace(/^mce\-/, ''); 11397 node.attr('type', type === 'text/javascript' ? null : type); 11398 11399 if (value.length > 0) { 11400 node.firstChild.value = '// <![CDATA[\n' + trim(value) + '\n// ]]>'; 11401 } 11402 } else { 11403 if (value.length > 0) { 11404 node.firstChild.value = '<!--\n' + trim(value) + '\n-->'; 11405 } 11406 } 11407 } 11408 }); 11409 11410 // Convert comments to cdata and handle protected comments 11411 htmlParser.addNodeFilter('#comment', function(nodes) { 11412 var i = nodes.length, node; 11413 11414 while (i--) { 11415 node = nodes[i]; 11416 11417 if (node.value.indexOf('[CDATA[') === 0) { 11418 node.name = '#cdata'; 11419 node.type = 4; 11420 node.value = node.value.replace(/^\[CDATA\[|\]\]$/g, ''); 11421 } else if (node.value.indexOf('mce:protected ') === 0) { 11422 node.name = "#text"; 11423 node.type = 3; 11424 node.raw = true; 11425 node.value = unescape(node.value).substr(14); 11426 } 11427 } 11428 }); 11429 11430 htmlParser.addNodeFilter('xml:namespace,input', function(nodes, name) { 11431 var i = nodes.length, node; 11432 11433 while (i--) { 11434 node = nodes[i]; 11435 if (node.type === 7) { 11436 node.remove(); 11437 } else if (node.type === 1) { 11438 if (name === "input" && !("type" in node.attributes.map)) { 11439 node.attr('type', 'text'); 11440 } 11441 } 11442 } 11443 }); 11444 11445 // Fix list elements, TODO: Replace this later 11446 if (settings.fix_list_elements) { 11447 htmlParser.addNodeFilter('ul,ol', function(nodes) { 11448 var i = nodes.length, node, parentNode; 11449 11450 while (i--) { 11451 node = nodes[i]; 11452 parentNode = node.parent; 11453 11454 if (parentNode.name === 'ul' || parentNode.name === 'ol') { 11455 if (node.prev && node.prev.name === 'li') { 11456 node.prev.append(node); 11457 } 11458 } 11459 } 11460 }); 11461 } 11462 11463 // Remove internal data attributes 11464 htmlParser.addAttributeFilter('data-mce-src,data-mce-href,data-mce-style,data-mce-selected', function(nodes, name) { 11465 var i = nodes.length; 11466 11467 while (i--) { 11468 nodes[i].attr(name, null); 11469 } 11470 }); 11471 11472 // Return public methods 11473 return { 11474 /** 11475 * Schema instance that was used to when the Serializer was constructed. 11476 * 11477 * @field {tinymce.html.Schema} schema 11478 */ 11479 schema: schema, 11480 11481 /** 11482 * Adds a node filter function to the parser used by the serializer, the parser will collect the specified nodes by name 11483 * and then execute the callback ones it has finished parsing the document. 11484 * 11485 * @example 11486 * parser.addNodeFilter('p,h1', function(nodes, name) { 11487 * for (var i = 0; i < nodes.length; i++) { 11488 * console.log(nodes[i].name); 11489 * } 11490 * }); 11491 * @method addNodeFilter 11492 * @method {String} name Comma separated list of nodes to collect. 11493 * @param {function} callback Callback function to execute once it has collected nodes. 11494 */ 11495 addNodeFilter: htmlParser.addNodeFilter, 11496 11497 /** 11498 * Adds a attribute filter function to the parser used by the serializer, the parser will 11499 * collect nodes that has the specified attributes 11500 * and then execute the callback ones it has finished parsing the document. 11501 * 11502 * @example 11503 * parser.addAttributeFilter('src,href', function(nodes, name) { 11504 * for (var i = 0; i < nodes.length; i++) { 11505 * console.log(nodes[i].name); 11506 * } 11507 * }); 11508 * @method addAttributeFilter 11509 * @method {String} name Comma separated list of nodes to collect. 11510 * @param {function} callback Callback function to execute once it has collected nodes. 11511 */ 11512 addAttributeFilter: htmlParser.addAttributeFilter, 11513 11514 /** 11515 * Serializes the specified browser DOM node into a HTML string. 11516 * 11517 * @method serialize 11518 * @param {DOMNode} node DOM node to serialize. 11519 * @param {Object} args Arguments option that gets passed to event handlers. 11520 */ 11521 serialize: function(node, args) { 11522 var self = this, impl, doc, oldDoc, htmlSerializer, content; 11523 11524 // Explorer won't clone contents of script and style and the 11525 // selected index of select elements are cleared on a clone operation. 11526 if (Env.ie && dom.select('script,style,select,map').length > 0) { 11527 content = node.innerHTML; 11528 node = node.cloneNode(false); 11529 dom.setHTML(node, content); 11530 } else { 11531 node = node.cloneNode(true); 11532 } 11533 11534 // Nodes needs to be attached to something in WebKit/Opera 11535 // This fix will make DOM ranges and make Sizzle happy! 11536 impl = node.ownerDocument.implementation; 11537 if (impl.createHTMLDocument) { 11538 // Create an empty HTML document 11539 doc = impl.createHTMLDocument(""); 11540 11541 // Add the element or it's children if it's a body element to the new document 11542 each(node.nodeName == 'BODY' ? node.childNodes : [node], function(node) { 11543 doc.body.appendChild(doc.importNode(node, true)); 11544 }); 11545 11546 // Grab first child or body element for serialization 11547 if (node.nodeName != 'BODY') { 11548 node = doc.body.firstChild; 11549 } else { 11550 node = doc.body; 11551 } 11552 11553 // set the new document in DOMUtils so createElement etc works 11554 oldDoc = dom.doc; 11555 dom.doc = doc; 11556 } 11557 11558 args = args || {}; 11559 args.format = args.format || 'html'; 11560 11561 // Don't wrap content if we want selected html 11562 if (args.selection) { 11563 args.forced_root_block = ''; 11564 } 11565 11566 // Pre process 11567 if (!args.no_events) { 11568 args.node = node; 11569 self.onPreProcess(args); 11570 } 11571 11572 // Setup serializer 11573 htmlSerializer = new Serializer(settings, schema); 11574 11575 // Parse and serialize HTML 11576 args.content = htmlSerializer.serialize( 11577 htmlParser.parse(trim(args.getInner ? node.innerHTML : dom.getOuterHTML(node)), args) 11578 ); 11579 11580 // Replace all BOM characters for now until we can find a better solution 11581 if (!args.cleanup) { 11582 args.content = args.content.replace(/\uFEFF/g, ''); 11583 } 11584 11585 // Post process 11586 if (!args.no_events) { 11587 self.onPostProcess(args); 11588 } 11589 11590 // Restore the old document if it was changed 11591 if (oldDoc) { 11592 dom.doc = oldDoc; 11593 } 11594 11595 args.node = null; 11596 11597 return args.content; 11598 }, 11599 11600 /** 11601 * Adds valid elements rules to the serializers schema instance this enables you to specify things 11602 * like what elements should be outputted and what attributes specific elements might have. 11603 * Consult the Wiki for more details on this format. 11604 * 11605 * @method addRules 11606 * @param {String} rules Valid elements rules string to add to schema. 11607 */ 11608 addRules: function(rules) { 11609 schema.addValidElements(rules); 11610 }, 11611 11612 /** 11613 * Sets the valid elements rules to the serializers schema instance this enables you to specify things 11614 * like what elements should be outputted and what attributes specific elements might have. 11615 * Consult the Wiki for more details on this format. 11616 * 11617 * @method setRules 11618 * @param {String} rules Valid elements rules string. 11619 */ 11620 setRules: function(rules) { 11621 schema.setValidElements(rules); 11622 }, 11623 11624 onPreProcess: function(args) { 11625 if (editor) { 11626 editor.fire('PreProcess', args); 11627 } 11628 }, 11629 11630 onPostProcess: function(args) { 11631 if (editor) { 11632 editor.fire('PostProcess', args); 11633 } 11634 } 11635 }; 11636 }; 11637 }); 11638 11639 // Included from: js/tinymce/classes/dom/TridentSelection.js 11640 11641 /** 11642 * TridentSelection.js 11643 * 11644 * Copyright, Moxiecode Systems AB 11645 * Released under LGPL License. 11646 * 11647 * License: http://www.tinymce.com/license 11648 * Contributing: http://www.tinymce.com/contributing 11649 */ 11650 11651 /** 11652 * Selection class for old explorer versions. This one fakes the 11653 * native selection object available on modern browsers. 11654 * 11655 * @class tinymce.dom.TridentSelection 11656 */ 11657 define("tinymce/dom/TridentSelection", [], function() { 11658 function Selection(selection) { 11659 var self = this, dom = selection.dom, FALSE = false; 11660 11661 function getPosition(rng, start) { 11662 var checkRng, startIndex = 0, endIndex, inside, 11663 children, child, offset, index, position = -1, parent; 11664 11665 // Setup test range, collapse it and get the parent 11666 checkRng = rng.duplicate(); 11667 checkRng.collapse(start); 11668 parent = checkRng.parentElement(); 11669 11670 // Check if the selection is within the right document 11671 if (parent.ownerDocument !== selection.dom.doc) { 11672 return; 11673 } 11674 11675 // IE will report non editable elements as it's parent so look for an editable one 11676 while (parent.contentEditable === "false") { 11677 parent = parent.parentNode; 11678 } 11679 11680 // If parent doesn't have any children then return that we are inside the element 11681 if (!parent.hasChildNodes()) { 11682 return {node: parent, inside: 1}; 11683 } 11684 11685 // Setup node list and endIndex 11686 children = parent.children; 11687 endIndex = children.length - 1; 11688 11689 // Perform a binary search for the position 11690 while (startIndex <= endIndex) { 11691 index = Math.floor((startIndex + endIndex) / 2); 11692 11693 // Move selection to node and compare the ranges 11694 child = children[index]; 11695 checkRng.moveToElementText(child); 11696 position = checkRng.compareEndPoints(start ? 'StartToStart' : 'EndToEnd', rng); 11697 11698 // Before/after or an exact match 11699 if (position > 0) { 11700 endIndex = index - 1; 11701 } else if (position < 0) { 11702 startIndex = index + 1; 11703 } else { 11704 return {node: child}; 11705 } 11706 } 11707 11708 // Check if child position is before or we didn't find a position 11709 if (position < 0) { 11710 // No element child was found use the parent element and the offset inside that 11711 if (!child) { 11712 checkRng.moveToElementText(parent); 11713 checkRng.collapse(true); 11714 child = parent; 11715 inside = true; 11716 } else { 11717 checkRng.collapse(false); 11718 } 11719 11720 // Walk character by character in text node until we hit the selected range endpoint, 11721 // hit the end of document or parent isn't the right one 11722 // We need to walk char by char since rng.text or rng.htmlText will trim line endings 11723 offset = 0; 11724 while (checkRng.compareEndPoints(start ? 'StartToStart' : 'StartToEnd', rng) !== 0) { 11725 if (checkRng.move('character', 1) === 0 || parent != checkRng.parentElement()) { 11726 break; 11727 } 11728 11729 offset++; 11730 } 11731 } else { 11732 // Child position is after the selection endpoint 11733 checkRng.collapse(true); 11734 11735 // Walk character by character in text node until we hit the selected range endpoint, hit 11736 // the end of document or parent isn't the right one 11737 offset = 0; 11738 while (checkRng.compareEndPoints(start ? 'StartToStart' : 'StartToEnd', rng) !== 0) { 11739 if (checkRng.move('character', -1) === 0 || parent != checkRng.parentElement()) { 11740 break; 11741 } 11742 11743 offset++; 11744 } 11745 } 11746 11747 return {node: child, position: position, offset: offset, inside: inside}; 11748 } 11749 11750 // Returns a W3C DOM compatible range object by using the IE Range API 11751 function getRange() { 11752 var ieRange = selection.getRng(), domRange = dom.createRng(), element, collapsed, tmpRange, element2, bookmark; 11753 11754 // If selection is outside the current document just return an empty range 11755 element = ieRange.item ? ieRange.item(0) : ieRange.parentElement(); 11756 if (element.ownerDocument != dom.doc) { 11757 return domRange; 11758 } 11759 11760 collapsed = selection.isCollapsed(); 11761 11762 // Handle control selection 11763 if (ieRange.item) { 11764 domRange.setStart(element.parentNode, dom.nodeIndex(element)); 11765 domRange.setEnd(domRange.startContainer, domRange.startOffset + 1); 11766 11767 return domRange; 11768 } 11769 11770 function findEndPoint(start) { 11771 var endPoint = getPosition(ieRange, start), container, offset, textNodeOffset = 0, sibling, undef, nodeValue; 11772 11773 container = endPoint.node; 11774 offset = endPoint.offset; 11775 11776 if (endPoint.inside && !container.hasChildNodes()) { 11777 domRange[start ? 'setStart' : 'setEnd'](container, 0); 11778 return; 11779 } 11780 11781 if (offset === undef) { 11782 domRange[start ? 'setStartBefore' : 'setEndAfter'](container); 11783 return; 11784 } 11785 11786 if (endPoint.position < 0) { 11787 sibling = endPoint.inside ? container.firstChild : container.nextSibling; 11788 11789 if (!sibling) { 11790 domRange[start ? 'setStartAfter' : 'setEndAfter'](container); 11791 return; 11792 } 11793 11794 if (!offset) { 11795 if (sibling.nodeType == 3) { 11796 domRange[start ? 'setStart' : 'setEnd'](sibling, 0); 11797 } else { 11798 domRange[start ? 'setStartBefore' : 'setEndBefore'](sibling); 11799 } 11800 11801 return; 11802 } 11803 11804 // Find the text node and offset 11805 while (sibling) { 11806 nodeValue = sibling.nodeValue; 11807 textNodeOffset += nodeValue.length; 11808 11809 // We are at or passed the position we where looking for 11810 if (textNodeOffset >= offset) { 11811 container = sibling; 11812 textNodeOffset -= offset; 11813 textNodeOffset = nodeValue.length - textNodeOffset; 11814 break; 11815 } 11816 11817 sibling = sibling.nextSibling; 11818 } 11819 } else { 11820 // Find the text node and offset 11821 sibling = container.previousSibling; 11822 11823 if (!sibling) { 11824 return domRange[start ? 'setStartBefore' : 'setEndBefore'](container); 11825 } 11826 11827 // If there isn't any text to loop then use the first position 11828 if (!offset) { 11829 if (container.nodeType == 3) { 11830 domRange[start ? 'setStart' : 'setEnd'](sibling, container.nodeValue.length); 11831 } else { 11832 domRange[start ? 'setStartAfter' : 'setEndAfter'](sibling); 11833 } 11834 11835 return; 11836 } 11837 11838 while (sibling) { 11839 textNodeOffset += sibling.nodeValue.length; 11840 11841 // We are at or passed the position we where looking for 11842 if (textNodeOffset >= offset) { 11843 container = sibling; 11844 textNodeOffset -= offset; 11845 break; 11846 } 11847 11848 sibling = sibling.previousSibling; 11849 } 11850 } 11851 11852 domRange[start ? 'setStart' : 'setEnd'](container, textNodeOffset); 11853 } 11854 11855 try { 11856 // Find start point 11857 findEndPoint(true); 11858 11859 // Find end point if needed 11860 if (!collapsed) { 11861 findEndPoint(); 11862 } 11863 } catch (ex) { 11864 // IE has a nasty bug where text nodes might throw "invalid argument" when you 11865 // access the nodeValue or other properties of text nodes. This seems to happend when 11866 // text nodes are split into two nodes by a delete/backspace call. So lets detect it and try to fix it. 11867 if (ex.number == -2147024809) { 11868 // Get the current selection 11869 bookmark = self.getBookmark(2); 11870 11871 // Get start element 11872 tmpRange = ieRange.duplicate(); 11873 tmpRange.collapse(true); 11874 element = tmpRange.parentElement(); 11875 11876 // Get end element 11877 if (!collapsed) { 11878 tmpRange = ieRange.duplicate(); 11879 tmpRange.collapse(false); 11880 element2 = tmpRange.parentElement(); 11881 element2.innerHTML = element2.innerHTML; 11882 } 11883 11884 // Remove the broken elements 11885 element.innerHTML = element.innerHTML; 11886 11887 // Restore the selection 11888 self.moveToBookmark(bookmark); 11889 11890 // Since the range has moved we need to re-get it 11891 ieRange = selection.getRng(); 11892 11893 // Find start point 11894 findEndPoint(true); 11895 11896 // Find end point if needed 11897 if (!collapsed) { 11898 findEndPoint(); 11899 } 11900 } else { 11901 throw ex; // Throw other errors 11902 } 11903 } 11904 11905 return domRange; 11906 } 11907 11908 this.getBookmark = function(type) { 11909 var rng = selection.getRng(), bookmark = {}; 11910 11911 function getIndexes(node) { 11912 var parent, root, children, i, indexes = []; 11913 11914 parent = node.parentNode; 11915 root = dom.getRoot().parentNode; 11916 11917 while (parent != root && parent.nodeType !== 9) { 11918 children = parent.children; 11919 11920 i = children.length; 11921 while (i--) { 11922 if (node === children[i]) { 11923 indexes.push(i); 11924 break; 11925 } 11926 } 11927 11928 node = parent; 11929 parent = parent.parentNode; 11930 } 11931 11932 return indexes; 11933 } 11934 11935 function getBookmarkEndPoint(start) { 11936 var position; 11937 11938 position = getPosition(rng, start); 11939 if (position) { 11940 return { 11941 position: position.position, 11942 offset: position.offset, 11943 indexes: getIndexes(position.node), 11944 inside: position.inside 11945 }; 11946 } 11947 } 11948 11949 // Non ubstructive bookmark 11950 if (type === 2) { 11951 // Handle text selection 11952 if (!rng.item) { 11953 bookmark.start = getBookmarkEndPoint(true); 11954 11955 if (!selection.isCollapsed()) { 11956 bookmark.end = getBookmarkEndPoint(); 11957 } 11958 } else { 11959 bookmark.start = {ctrl: true, indexes: getIndexes(rng.item(0))}; 11960 } 11961 } 11962 11963 return bookmark; 11964 }; 11965 11966 this.moveToBookmark = function(bookmark) { 11967 var rng, body = dom.doc.body; 11968 11969 function resolveIndexes(indexes) { 11970 var node, i, idx, children; 11971 11972 node = dom.getRoot(); 11973 for (i = indexes.length - 1; i >= 0; i--) { 11974 children = node.children; 11975 idx = indexes[i]; 11976 11977 if (idx <= children.length - 1) { 11978 node = children[idx]; 11979 } 11980 } 11981 11982 return node; 11983 } 11984 11985 function setBookmarkEndPoint(start) { 11986 var endPoint = bookmark[start ? 'start' : 'end'], moveLeft, moveRng, undef, offset; 11987 11988 if (endPoint) { 11989 moveLeft = endPoint.position > 0; 11990 11991 moveRng = body.createTextRange(); 11992 moveRng.moveToElementText(resolveIndexes(endPoint.indexes)); 11993 11994 offset = endPoint.offset; 11995 if (offset !== undef) { 11996 moveRng.collapse(endPoint.inside || moveLeft); 11997 moveRng.moveStart('character', moveLeft ? -offset : offset); 11998 } else { 11999 moveRng.collapse(start); 12000 } 12001 12002 rng.setEndPoint(start ? 'StartToStart' : 'EndToStart', moveRng); 12003 12004 if (start) { 12005 rng.collapse(true); 12006 } 12007 } 12008 } 12009 12010 if (bookmark.start) { 12011 if (bookmark.start.ctrl) { 12012 rng = body.createControlRange(); 12013 rng.addElement(resolveIndexes(bookmark.start.indexes)); 12014 rng.select(); 12015 } else { 12016 rng = body.createTextRange(); 12017 setBookmarkEndPoint(true); 12018 setBookmarkEndPoint(); 12019 rng.select(); 12020 } 12021 } 12022 }; 12023 12024 this.addRange = function(rng) { 12025 var ieRng, ctrlRng, startContainer, startOffset, endContainer, endOffset, sibling, 12026 doc = selection.dom.doc, body = doc.body, nativeRng, ctrlElm; 12027 12028 function setEndPoint(start) { 12029 var container, offset, marker, tmpRng, nodes; 12030 12031 marker = dom.create('a'); 12032 container = start ? startContainer : endContainer; 12033 offset = start ? startOffset : endOffset; 12034 tmpRng = ieRng.duplicate(); 12035 12036 if (container == doc || container == doc.documentElement) { 12037 container = body; 12038 offset = 0; 12039 } 12040 12041 if (container.nodeType == 3) { 12042 container.parentNode.insertBefore(marker, container); 12043 tmpRng.moveToElementText(marker); 12044 tmpRng.moveStart('character', offset); 12045 dom.remove(marker); 12046 ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng); 12047 } else { 12048 nodes = container.childNodes; 12049 12050 if (nodes.length) { 12051 if (offset >= nodes.length) { 12052 dom.insertAfter(marker, nodes[nodes.length - 1]); 12053 } else { 12054 container.insertBefore(marker, nodes[offset]); 12055 } 12056 12057 tmpRng.moveToElementText(marker); 12058 } else if (container.canHaveHTML) { 12059 // Empty node selection for example <div>|</div> 12060 // Setting innerHTML with a span marker then remove that marker seems to keep empty block elements open 12061 container.innerHTML = '<span></span>'; 12062 marker = container.firstChild; 12063 tmpRng.moveToElementText(marker); 12064 tmpRng.collapse(FALSE); // Collapse false works better than true for some odd reason 12065 } 12066 12067 ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng); 12068 dom.remove(marker); 12069 } 12070 } 12071 12072 // Setup some shorter versions 12073 startContainer = rng.startContainer; 12074 startOffset = rng.startOffset; 12075 endContainer = rng.endContainer; 12076 endOffset = rng.endOffset; 12077 ieRng = body.createTextRange(); 12078 12079 // If single element selection then try making a control selection out of it 12080 if (startContainer == endContainer && startContainer.nodeType == 1) { 12081 // Trick to place the caret inside an empty block element like <p></p> 12082 if (startOffset == endOffset && !startContainer.hasChildNodes()) { 12083 if (startContainer.canHaveHTML) { 12084 // Check if previous sibling is an empty block if it is then we need to render it 12085 // IE would otherwise move the caret into the sibling instead of the empty startContainer see: #5236 12086 // Example this: <p></p><p>|</p> would become this: <p>|</p><p></p> 12087 sibling = startContainer.previousSibling; 12088 if (sibling && !sibling.hasChildNodes() && dom.isBlock(sibling)) { 12089 sibling.innerHTML = ''; 12090 } else { 12091 sibling = null; 12092 } 12093 12094 startContainer.innerHTML = '<span></span><span></span>'; 12095 ieRng.moveToElementText(startContainer.lastChild); 12096 ieRng.select(); 12097 dom.doc.selection.clear(); 12098 startContainer.innerHTML = ''; 12099 12100 if (sibling) { 12101 sibling.innerHTML = ''; 12102 } 12103 return; 12104 } else { 12105 startOffset = dom.nodeIndex(startContainer); 12106 startContainer = startContainer.parentNode; 12107 } 12108 } 12109 12110 if (startOffset == endOffset - 1) { 12111 try { 12112 ctrlElm = startContainer.childNodes[startOffset]; 12113 ctrlRng = body.createControlRange(); 12114 ctrlRng.addElement(ctrlElm); 12115 ctrlRng.select(); 12116 12117 // Check if the range produced is on the correct element and is a control range 12118 // On IE 8 it will select the parent contentEditable container if you select an inner element see: #5398 12119 nativeRng = selection.getRng(); 12120 if (nativeRng.item && ctrlElm === nativeRng.item(0)) { 12121 return; 12122 } 12123 } catch (ex) { 12124 // Ignore 12125 } 12126 } 12127 } 12128 12129 // Set start/end point of selection 12130 setEndPoint(true); 12131 setEndPoint(); 12132 12133 // Select the new range and scroll it into view 12134 ieRng.select(); 12135 }; 12136 12137 // Expose range method 12138 this.getRangeAt = getRange; 12139 } 12140 12141 return Selection; 12142 }); 12143 12144 // Included from: js/tinymce/classes/util/VK.js 12145 12146 /** 12147 * VK.js 12148 * 12149 * Copyright, Moxiecode Systems AB 12150 * Released under LGPL License. 12151 * 12152 * License: http://www.tinymce.com/license 12153 * Contributing: http://www.tinymce.com/contributing 12154 */ 12155 12156 /** 12157 * This file exposes a set of the common KeyCodes for use. Please grow it as needed. 12158 */ 12159 define("tinymce/util/VK", [ 12160 "tinymce/Env" 12161 ], function(Env) { 12162 return { 12163 BACKSPACE: 8, 12164 DELETE: 46, 12165 DOWN: 40, 12166 ENTER: 13, 12167 LEFT: 37, 12168 RIGHT: 39, 12169 SPACEBAR: 32, 12170 TAB: 9, 12171 UP: 38, 12172 12173 modifierPressed: function(e) { 12174 return e.shiftKey || e.ctrlKey || e.altKey; 12175 }, 12176 12177 metaKeyPressed: function(e) { 12178 // Check if ctrl or meta key is pressed also check if alt is false for Polish users 12179 return (Env.mac ? e.metaKey : e.ctrlKey) && !e.altKey; 12180 } 12181 }; 12182 }); 12183 12184 // Included from: js/tinymce/classes/dom/ControlSelection.js 12185 12186 /** 12187 * ControlSelection.js 12188 * 12189 * Copyright, Moxiecode Systems AB 12190 * Released under LGPL License. 12191 * 12192 * License: http://www.tinymce.com/license 12193 * Contributing: http://www.tinymce.com/contributing 12194 */ 12195 12196 /** 12197 * This class handles control selection of elements. Controls are elements 12198 * that can be resized and needs to be selected as a whole. It adds custom resize handles 12199 * to all browser engines that support properly disabling the built in resize logic. 12200 * 12201 * @class tinymce.dom.ControlSelection 12202 */ 12203 define("tinymce/dom/ControlSelection", [ 12204 "tinymce/util/VK", 12205 "tinymce/util/Tools", 12206 "tinymce/Env" 12207 ], function(VK, Tools, Env) { 12208 return function(selection, editor) { 12209 var dom = editor.dom, each = Tools.each; 12210 var selectedElm, selectedElmGhost, resizeHandles, selectedHandle, lastMouseDownEvent; 12211 var startX, startY, selectedElmX, selectedElmY, startW, startH, ratio, resizeStarted; 12212 var width, height, editableDoc = editor.getDoc(), rootDocument = document, isIE = Env.ie && Env.ie < 11; 12213 12214 // Details about each resize handle how to scale etc 12215 resizeHandles = { 12216 // Name: x multiplier, y multiplier, delta size x, delta size y 12217 n: [0.5, 0, 0, -1], 12218 e: [1, 0.5, 1, 0], 12219 s: [0.5, 1, 0, 1], 12220 w: [0, 0.5, -1, 0], 12221 nw: [0, 0, -1, -1], 12222 ne: [1, 0, 1, -1], 12223 se: [1, 1, 1, 1], 12224 sw: [0, 1, -1, 1] 12225 }; 12226 12227 // Add CSS for resize handles, cloned element and selected 12228 var rootClass = '.mce-content-body'; 12229 editor.contentStyles.push( 12230 rootClass + ' div.mce-resizehandle {' + 12231 'position: absolute;' + 12232 'border: 1px solid black;' + 12233 'background: #FFF;' + 12234 'width: 5px;' + 12235 'height: 5px;' + 12236 'z-index: 10000' + 12237 '}' + 12238 rootClass + ' .mce-resizehandle:hover {' + 12239 'background: #000' + 12240 '}' + 12241 rootClass + ' img[data-mce-selected], hr[data-mce-selected] {' + 12242 'outline: 1px solid black;' + 12243 'resize: none' + // Have been talks about implementing this in browsers 12244 '}' + 12245 rootClass + ' .mce-clonedresizable {' + 12246 'position: absolute;' + 12247 (Env.gecko ? '' : 'outline: 1px dashed black;') + // Gecko produces trails while resizing 12248 'opacity: .5;' + 12249 'filter: alpha(opacity=50);' + 12250 'z-index: 10000' + 12251 '}' 12252 ); 12253 12254 function isResizable(elm) { 12255 var selector = editor.settings.object_resizing; 12256 12257 if (selector === false || Env.iOS) { 12258 return false; 12259 } 12260 12261 if (typeof selector != 'string') { 12262 selector = 'table,img,div'; 12263 } 12264 12265 if (elm.getAttribute('data-mce-resize') === 'false') { 12266 return false; 12267 } 12268 12269 return editor.dom.is(elm, selector); 12270 } 12271 12272 function resizeGhostElement(e) { 12273 var deltaX, deltaY; 12274 12275 // Calc new width/height 12276 deltaX = e.screenX - startX; 12277 deltaY = e.screenY - startY; 12278 12279 // Calc new size 12280 width = deltaX * selectedHandle[2] + startW; 12281 height = deltaY * selectedHandle[3] + startH; 12282 12283 // Never scale down lower than 5 pixels 12284 width = width < 5 ? 5 : width; 12285 height = height < 5 ? 5 : height; 12286 12287 // Constrain proportions when modifier key is pressed or if the nw, ne, sw, se corners are moved on an image 12288 if (VK.modifierPressed(e) || (selectedElm.nodeName == "IMG" && selectedHandle[2] * selectedHandle[3] !== 0)) { 12289 width = Math.round(height / ratio); 12290 height = Math.round(width * ratio); 12291 } 12292 12293 // Update ghost size 12294 dom.setStyles(selectedElmGhost, { 12295 width: width, 12296 height: height 12297 }); 12298 12299 // Update ghost X position if needed 12300 if (selectedHandle[2] < 0 && selectedElmGhost.clientWidth <= width) { 12301 dom.setStyle(selectedElmGhost, 'left', selectedElmX + (startW - width)); 12302 } 12303 12304 // Update ghost Y position if needed 12305 if (selectedHandle[3] < 0 && selectedElmGhost.clientHeight <= height) { 12306 dom.setStyle(selectedElmGhost, 'top', selectedElmY + (startH - height)); 12307 } 12308 12309 if (!resizeStarted) { 12310 editor.fire('ObjectResizeStart', {target: selectedElm, width: startW, height: startH}); 12311 resizeStarted = true; 12312 } 12313 } 12314 12315 function endGhostResize() { 12316 resizeStarted = false; 12317 12318 function setSizeProp(name, value) { 12319 if (value) { 12320 // Resize by using style or attribute 12321 if (selectedElm.style[name] || !editor.schema.isValid(selectedElm.nodeName.toLowerCase(), name)) { 12322 dom.setStyle(selectedElm, name, value); 12323 } else { 12324 dom.setAttrib(selectedElm, name, value); 12325 } 12326 } 12327 } 12328 12329 // Set width/height properties 12330 setSizeProp('width', width); 12331 setSizeProp('height', height); 12332 12333 dom.unbind(editableDoc, 'mousemove', resizeGhostElement); 12334 dom.unbind(editableDoc, 'mouseup', endGhostResize); 12335 12336 if (rootDocument != editableDoc) { 12337 dom.unbind(rootDocument, 'mousemove', resizeGhostElement); 12338 dom.unbind(rootDocument, 'mouseup', endGhostResize); 12339 } 12340 12341 // Remove ghost and update resize handle positions 12342 dom.remove(selectedElmGhost); 12343 12344 if (!isIE || selectedElm.nodeName == "TABLE") { 12345 showResizeRect(selectedElm); 12346 } 12347 12348 editor.fire('ObjectResized', {target: selectedElm, width: width, height: height}); 12349 editor.nodeChanged(); 12350 } 12351 12352 function showResizeRect(targetElm, mouseDownHandleName, mouseDownEvent) { 12353 var position, targetWidth, targetHeight, e, rect, offsetParent = editor.getBody(); 12354 12355 unbindResizeHandleEvents(); 12356 12357 // Get position and size of target 12358 position = dom.getPos(targetElm, offsetParent); 12359 selectedElmX = position.x; 12360 selectedElmY = position.y; 12361 rect = targetElm.getBoundingClientRect(); // Fix for Gecko offsetHeight for table with caption 12362 targetWidth = rect.width || (rect.right - rect.left); 12363 targetHeight = rect.height || (rect.bottom - rect.top); 12364 12365 // Reset width/height if user selects a new image/table 12366 if (selectedElm != targetElm) { 12367 detachResizeStartListener(); 12368 selectedElm = targetElm; 12369 width = height = 0; 12370 } 12371 12372 // Makes it possible to disable resizing 12373 e = editor.fire('ObjectSelected', {target: targetElm}); 12374 12375 if (isResizable(targetElm) && !e.isDefaultPrevented()) { 12376 each(resizeHandles, function(handle, name) { 12377 var handleElm, handlerContainerElm; 12378 12379 function startDrag(e) { 12380 startX = e.screenX; 12381 startY = e.screenY; 12382 startW = selectedElm.clientWidth; 12383 startH = selectedElm.clientHeight; 12384 ratio = startH / startW; 12385 selectedHandle = handle; 12386 12387 selectedElmGhost = selectedElm.cloneNode(true); 12388 dom.addClass(selectedElmGhost, 'mce-clonedresizable'); 12389 selectedElmGhost.contentEditable = false; // Hides IE move layer cursor 12390 selectedElmGhost.unSelectabe = true; 12391 dom.setStyles(selectedElmGhost, { 12392 left: selectedElmX, 12393 top: selectedElmY, 12394 margin: 0 12395 }); 12396 12397 selectedElmGhost.removeAttribute('data-mce-selected'); 12398 editor.getBody().appendChild(selectedElmGhost); 12399 12400 dom.bind(editableDoc, 'mousemove', resizeGhostElement); 12401 dom.bind(editableDoc, 'mouseup', endGhostResize); 12402 12403 if (rootDocument != editableDoc) { 12404 dom.bind(rootDocument, 'mousemove', resizeGhostElement); 12405 dom.bind(rootDocument, 'mouseup', endGhostResize); 12406 } 12407 } 12408 12409 if (mouseDownHandleName) { 12410 // Drag started by IE native resizestart 12411 if (name == mouseDownHandleName) { 12412 startDrag(mouseDownEvent); 12413 } 12414 12415 return; 12416 } 12417 12418 // Get existing or render resize handle 12419 handleElm = dom.get('mceResizeHandle' + name); 12420 if (!handleElm) { 12421 handlerContainerElm = editor.getBody(); 12422 12423 handleElm = dom.add(handlerContainerElm, 'div', { 12424 id: 'mceResizeHandle' + name, 12425 'data-mce-bogus': true, 12426 'class': 'mce-resizehandle', 12427 unselectable: true, 12428 style: 'cursor:' + name + '-resize; margin:0; padding:0' 12429 }); 12430 12431 // Hides IE move layer cursor 12432 // If we set it on Chrome we get this wounderful bug: #6725 12433 if (Env.ie) { 12434 handleElm.contentEditable = false; 12435 } 12436 } else { 12437 dom.show(handleElm); 12438 } 12439 12440 if (!handle.elm) { 12441 dom.bind(handleElm, 'mousedown', function(e) { 12442 e.stopImmediatePropagation(); 12443 e.preventDefault(); 12444 startDrag(e); 12445 }); 12446 12447 handle.elm = handleElm; 12448 } 12449 12450 /* 12451 var halfHandleW = handleElm.offsetWidth / 2; 12452 var halfHandleH = handleElm.offsetHeight / 2; 12453 12454 // Position element 12455 dom.setStyles(handleElm, { 12456 left: Math.floor((targetWidth * handle[0] + selectedElmX) - halfHandleW + (handle[2] * halfHandleW)), 12457 top: Math.floor((targetHeight * handle[1] + selectedElmY) - halfHandleH + (handle[3] * halfHandleH)) 12458 }); 12459 */ 12460 12461 // Position element 12462 dom.setStyles(handleElm, { 12463 left: (targetWidth * handle[0] + selectedElmX) - (handleElm.offsetWidth / 2), 12464 top: (targetHeight * handle[1] + selectedElmY) - (handleElm.offsetHeight / 2) 12465 }); 12466 }); 12467 } else { 12468 hideResizeRect(); 12469 } 12470 12471 selectedElm.setAttribute('data-mce-selected', '1'); 12472 } 12473 12474 function hideResizeRect() { 12475 var name, handleElm; 12476 12477 unbindResizeHandleEvents(); 12478 12479 if (selectedElm) { 12480 selectedElm.removeAttribute('data-mce-selected'); 12481 } 12482 12483 for (name in resizeHandles) { 12484 handleElm = dom.get('mceResizeHandle' + name); 12485 if (handleElm) { 12486 dom.unbind(handleElm); 12487 dom.remove(handleElm); 12488 } 12489 } 12490 } 12491 12492 function updateResizeRect(e) { 12493 var controlElm; 12494 12495 function isChildOrEqual(node, parent) { 12496 if (node) { 12497 do { 12498 if (node === parent) { 12499 return true; 12500 } 12501 } while ((node = node.parentNode)); 12502 } 12503 } 12504 12505 // Remove data-mce-selected from all elements since they might have been copied using Ctrl+c/v 12506 each(dom.select('img[data-mce-selected],hr[data-mce-selected]'), function(img) { 12507 img.removeAttribute('data-mce-selected'); 12508 }); 12509 12510 controlElm = e.type == 'mousedown' ? e.target : selection.getNode(); 12511 controlElm = dom.getParent(controlElm, isIE ? 'table' : 'table,img,hr'); 12512 12513 if (isChildOrEqual(controlElm, editor.getBody())) { 12514 disableGeckoResize(); 12515 12516 if (isChildOrEqual(selection.getStart(), controlElm) && isChildOrEqual(selection.getEnd(), controlElm)) { 12517 if (!isIE || (controlElm != selection.getStart() && selection.getStart().nodeName !== 'IMG')) { 12518 showResizeRect(controlElm); 12519 return; 12520 } 12521 } 12522 } 12523 12524 hideResizeRect(); 12525 } 12526 12527 function attachEvent(elm, name, func) { 12528 if (elm && elm.attachEvent) { 12529 elm.attachEvent('on' + name, func); 12530 } 12531 } 12532 12533 function detachEvent(elm, name, func) { 12534 if (elm && elm.detachEvent) { 12535 elm.detachEvent('on' + name, func); 12536 } 12537 } 12538 12539 function resizeNativeStart(e) { 12540 var target = e.srcElement, pos, name, corner, cornerX, cornerY, relativeX, relativeY; 12541 12542 pos = target.getBoundingClientRect(); 12543 relativeX = lastMouseDownEvent.clientX - pos.left; 12544 relativeY = lastMouseDownEvent.clientY - pos.top; 12545 12546 // Figure out what corner we are draging on 12547 for (name in resizeHandles) { 12548 corner = resizeHandles[name]; 12549 12550 cornerX = target.offsetWidth * corner[0]; 12551 cornerY = target.offsetHeight * corner[1]; 12552 12553 if (Math.abs(cornerX - relativeX) < 8 && Math.abs(cornerY - relativeY) < 8) { 12554 selectedHandle = corner; 12555 break; 12556 } 12557 } 12558 12559 // Remove native selection and let the magic begin 12560 resizeStarted = true; 12561 editor.getDoc().selection.empty(); 12562 showResizeRect(target, name, lastMouseDownEvent); 12563 } 12564 12565 function nativeControlSelect(e) { 12566 var target = e.srcElement; 12567 12568 if (target != selectedElm) { 12569 detachResizeStartListener(); 12570 12571 if (target.id.indexOf('mceResizeHandle') === 0) { 12572 e.returnValue = false; 12573 return; 12574 } 12575 12576 if (target.nodeName == 'IMG' || target.nodeName == 'TABLE') { 12577 hideResizeRect(); 12578 selectedElm = target; 12579 attachEvent(target, 'resizestart', resizeNativeStart); 12580 } 12581 } 12582 } 12583 12584 function detachResizeStartListener() { 12585 detachEvent(selectedElm, 'resizestart', resizeNativeStart); 12586 } 12587 12588 function unbindResizeHandleEvents() { 12589 for (var name in resizeHandles) { 12590 var handle = resizeHandles[name]; 12591 12592 if (handle.elm) { 12593 dom.unbind(handle.elm); 12594 delete handle.elm; 12595 } 12596 } 12597 } 12598 12599 function disableGeckoResize() { 12600 try { 12601 // Disable object resizing on Gecko 12602 editor.getDoc().execCommand('enableObjectResizing', false, false); 12603 } catch (ex) { 12604 // Ignore 12605 } 12606 } 12607 12608 function controlSelect(elm) { 12609 var ctrlRng; 12610 12611 if (!isIE) { 12612 return; 12613 } 12614 12615 ctrlRng = editableDoc.body.createControlRange(); 12616 12617 try { 12618 ctrlRng.addElement(elm); 12619 ctrlRng.select(); 12620 return true; 12621 } catch (ex) { 12622 // Ignore since the element can't be control selected for example a P tag 12623 } 12624 } 12625 12626 editor.on('init', function() { 12627 if (isIE) { 12628 // Hide the resize rect on resize and reselect the image 12629 editor.on('ObjectResized', function(e) { 12630 if (e.target.nodeName != 'TABLE') { 12631 hideResizeRect(); 12632 controlSelect(e.target); 12633 } 12634 }); 12635 12636 attachEvent(editor.getBody(), 'controlselect', nativeControlSelect); 12637 12638 editor.on('mousedown', function(e) { 12639 lastMouseDownEvent = e; 12640 }); 12641 } else { 12642 disableGeckoResize(); 12643 12644 if (Env.ie >= 11) { 12645 // TODO: Drag/drop doesn't work 12646 editor.on('mouseup', function(e) { 12647 var nodeName = e.target.nodeName; 12648 12649 if (/^(TABLE|IMG|HR)$/.test(nodeName)) { 12650 editor.selection.select(e.target, nodeName == 'TABLE'); 12651 editor.nodeChanged(); 12652 } 12653 }); 12654 12655 editor.dom.bind(editor.getBody(), 'mscontrolselect', function(e) { 12656 if (/^(TABLE|IMG|HR)$/.test(e.target.nodeName)) { 12657 e.preventDefault(); 12658 12659 // This moves the selection from being a control selection to a text like selection like in WebKit #6753 12660 // TODO: Fix this the day IE works like other browsers without this nasty native ugly control selections. 12661 if (e.target.tagName == 'IMG') { 12662 window.setTimeout(function() { 12663 editor.selection.select(e.target); 12664 }, 0); 12665 } 12666 } 12667 }); 12668 } 12669 } 12670 12671 editor.on('nodechange mousedown mouseup ResizeEditor', updateResizeRect); 12672 12673 // Update resize rect while typing in a table 12674 editor.on('keydown keyup', function(e) { 12675 if (selectedElm && selectedElm.nodeName == "TABLE") { 12676 updateResizeRect(e); 12677 } 12678 }); 12679 12680 editor.on('hide', hideResizeRect); 12681 12682 // Hide rect on focusout since it would float on top of windows otherwise 12683 //editor.on('focusout', hideResizeRect); 12684 }); 12685 12686 editor.on('remove', unbindResizeHandleEvents); 12687 12688 function destroy() { 12689 selectedElm = selectedElmGhost = null; 12690 12691 if (isIE) { 12692 detachResizeStartListener(); 12693 detachEvent(editor.getBody(), 'controlselect', nativeControlSelect); 12694 } 12695 } 12696 12697 return { 12698 isResizable: isResizable, 12699 showResizeRect: showResizeRect, 12700 hideResizeRect: hideResizeRect, 12701 updateResizeRect: updateResizeRect, 12702 controlSelect: controlSelect, 12703 destroy: destroy 12704 }; 12705 }; 12706 }); 12707 12708 // Included from: js/tinymce/classes/dom/RangeUtils.js 12709 12710 /** 12711 * Range.js 12712 * 12713 * Copyright, Moxiecode Systems AB 12714 * Released under LGPL License. 12715 * 12716 * License: http://www.tinymce.com/license 12717 * Contributing: http://www.tinymce.com/contributing 12718 */ 12719 12720 /** 12721 * RangeUtils 12722 * 12723 * @class tinymce.dom.RangeUtils 12724 * @private 12725 */ 12726 define("tinymce/dom/RangeUtils", [ 12727 "tinymce/util/Tools", 12728 "tinymce/dom/TreeWalker" 12729 ], function(Tools, TreeWalker) { 12730 var each = Tools.each; 12731 12732 function RangeUtils(dom) { 12733 /** 12734 * Walks the specified range like object and executes the callback for each sibling collection it finds. 12735 * 12736 * @method walk 12737 * @param {Object} rng Range like object. 12738 * @param {function} callback Callback function to execute for each sibling collection. 12739 */ 12740 this.walk = function(rng, callback) { 12741 var startContainer = rng.startContainer, 12742 startOffset = rng.startOffset, 12743 endContainer = rng.endContainer, 12744 endOffset = rng.endOffset, 12745 ancestor, startPoint, 12746 endPoint, node, parent, siblings, nodes; 12747 12748 // Handle table cell selection the table plugin enables 12749 // you to fake select table cells and perform formatting actions on them 12750 nodes = dom.select('td.mce-item-selected,th.mce-item-selected'); 12751 if (nodes.length > 0) { 12752 each(nodes, function(node) { 12753 callback([node]); 12754 }); 12755 12756 return; 12757 } 12758 12759 /** 12760 * Excludes start/end text node if they are out side the range 12761 * 12762 * @private 12763 * @param {Array} nodes Nodes to exclude items from. 12764 * @return {Array} Array with nodes excluding the start/end container if needed. 12765 */ 12766 function exclude(nodes) { 12767 var node; 12768 12769 // First node is excluded 12770 node = nodes[0]; 12771 if (node.nodeType === 3 && node === startContainer && startOffset >= node.nodeValue.length) { 12772 nodes.splice(0, 1); 12773 } 12774 12775 // Last node is excluded 12776 node = nodes[nodes.length - 1]; 12777 if (endOffset === 0 && nodes.length > 0 && node === endContainer && node.nodeType === 3) { 12778 nodes.splice(nodes.length - 1, 1); 12779 } 12780 12781 return nodes; 12782 } 12783 12784 /** 12785 * Collects siblings 12786 * 12787 * @private 12788 * @param {Node} node Node to collect siblings from. 12789 * @param {String} name Name of the sibling to check for. 12790 * @return {Array} Array of collected siblings. 12791 */ 12792 function collectSiblings(node, name, end_node) { 12793 var siblings = []; 12794 12795 for (; node && node != end_node; node = node[name]) { 12796 siblings.push(node); 12797 } 12798 12799 return siblings; 12800 } 12801 12802 /** 12803 * Find an end point this is the node just before the common ancestor root. 12804 * 12805 * @private 12806 * @param {Node} node Node to start at. 12807 * @param {Node} root Root/ancestor element to stop just before. 12808 * @return {Node} Node just before the root element. 12809 */ 12810 function findEndPoint(node, root) { 12811 do { 12812 if (node.parentNode == root) { 12813 return node; 12814 } 12815 12816 node = node.parentNode; 12817 } while(node); 12818 } 12819 12820 function walkBoundary(start_node, end_node, next) { 12821 var siblingName = next ? 'nextSibling' : 'previousSibling'; 12822 12823 for (node = start_node, parent = node.parentNode; node && node != end_node; node = parent) { 12824 parent = node.parentNode; 12825 siblings = collectSiblings(node == start_node ? node : node[siblingName], siblingName); 12826 12827 if (siblings.length) { 12828 if (!next) { 12829 siblings.reverse(); 12830 } 12831 12832 callback(exclude(siblings)); 12833 } 12834 } 12835 } 12836 12837 // If index based start position then resolve it 12838 if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) { 12839 startContainer = startContainer.childNodes[startOffset]; 12840 } 12841 12842 // If index based end position then resolve it 12843 if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) { 12844 endContainer = endContainer.childNodes[Math.min(endOffset - 1, endContainer.childNodes.length - 1)]; 12845 } 12846 12847 // Same container 12848 if (startContainer == endContainer) { 12849 return callback(exclude([startContainer])); 12850 } 12851 12852 // Find common ancestor and end points 12853 ancestor = dom.findCommonAncestor(startContainer, endContainer); 12854 12855 // Process left side 12856 for (node = startContainer; node; node = node.parentNode) { 12857 if (node === endContainer) { 12858 return walkBoundary(startContainer, ancestor, true); 12859 } 12860 12861 if (node === ancestor) { 12862 break; 12863 } 12864 } 12865 12866 // Process right side 12867 for (node = endContainer; node; node = node.parentNode) { 12868 if (node === startContainer) { 12869 return walkBoundary(endContainer, ancestor); 12870 } 12871 12872 if (node === ancestor) { 12873 break; 12874 } 12875 } 12876 12877 // Find start/end point 12878 startPoint = findEndPoint(startContainer, ancestor) || startContainer; 12879 endPoint = findEndPoint(endContainer, ancestor) || endContainer; 12880 12881 // Walk left leaf 12882 walkBoundary(startContainer, startPoint, true); 12883 12884 // Walk the middle from start to end point 12885 siblings = collectSiblings( 12886 startPoint == startContainer ? startPoint : startPoint.nextSibling, 12887 'nextSibling', 12888 endPoint == endContainer ? endPoint.nextSibling : endPoint 12889 ); 12890 12891 if (siblings.length) { 12892 callback(exclude(siblings)); 12893 } 12894 12895 // Walk right leaf 12896 walkBoundary(endContainer, endPoint); 12897 }; 12898 12899 /** 12900 * Splits the specified range at it's start/end points. 12901 * 12902 * @private 12903 * @param {Range/RangeObject} rng Range to split. 12904 * @return {Object} Range position object. 12905 */ 12906 this.split = function(rng) { 12907 var startContainer = rng.startContainer, 12908 startOffset = rng.startOffset, 12909 endContainer = rng.endContainer, 12910 endOffset = rng.endOffset; 12911 12912 function splitText(node, offset) { 12913 return node.splitText(offset); 12914 } 12915 12916 // Handle single text node 12917 if (startContainer == endContainer && startContainer.nodeType == 3) { 12918 if (startOffset > 0 && startOffset < startContainer.nodeValue.length) { 12919 endContainer = splitText(startContainer, startOffset); 12920 startContainer = endContainer.previousSibling; 12921 12922 if (endOffset > startOffset) { 12923 endOffset = endOffset - startOffset; 12924 startContainer = endContainer = splitText(endContainer, endOffset).previousSibling; 12925 endOffset = endContainer.nodeValue.length; 12926 startOffset = 0; 12927 } else { 12928 endOffset = 0; 12929 } 12930 } 12931 } else { 12932 // Split startContainer text node if needed 12933 if (startContainer.nodeType == 3 && startOffset > 0 && startOffset < startContainer.nodeValue.length) { 12934 startContainer = splitText(startContainer, startOffset); 12935 startOffset = 0; 12936 } 12937 12938 // Split endContainer text node if needed 12939 if (endContainer.nodeType == 3 && endOffset > 0 && endOffset < endContainer.nodeValue.length) { 12940 endContainer = splitText(endContainer, endOffset).previousSibling; 12941 endOffset = endContainer.nodeValue.length; 12942 } 12943 } 12944 12945 return { 12946 startContainer: startContainer, 12947 startOffset: startOffset, 12948 endContainer: endContainer, 12949 endOffset: endOffset 12950 }; 12951 }; 12952 12953 /** 12954 * Normalizes the specified range by finding the closest best suitable caret location. 12955 * 12956 * @private 12957 * @param {Range} rng Range to normalize. 12958 * @return {Boolean} True/false if the specified range was normalized or not. 12959 */ 12960 this.normalize = function(rng) { 12961 var normalized, collapsed; 12962 12963 function normalizeEndPoint(start) { 12964 var container, offset, walker, body = dom.getRoot(), node, nonEmptyElementsMap; 12965 var directionLeft, isAfterNode; 12966 12967 function hasBrBeforeAfter(node, left) { 12968 var walker = new TreeWalker(node, dom.getParent(node.parentNode, dom.isBlock) || body); 12969 12970 while ((node = walker[left ? 'prev' : 'next']())) { 12971 if (node.nodeName === "BR") { 12972 return true; 12973 } 12974 } 12975 } 12976 12977 function isPrevNode(node, name) { 12978 return node.previousSibling && node.previousSibling.nodeName == name; 12979 } 12980 12981 // Walks the dom left/right to find a suitable text node to move the endpoint into 12982 // It will only walk within the current parent block or body and will stop if it hits a block or a BR/IMG 12983 function findTextNodeRelative(left, startNode) { 12984 var walker, lastInlineElement, parentBlockContainer; 12985 12986 startNode = startNode || container; 12987 parentBlockContainer = dom.getParent(startNode.parentNode, dom.isBlock) || body; 12988 12989 // Lean left before the BR element if it's the only BR within a block element. Gecko bug: #6680 12990 // This: <p><br>|</p> becomes <p>|<br></p> 12991 if (left && startNode.nodeName == 'BR' && isAfterNode && dom.isEmpty(parentBlockContainer)) { 12992 container = startNode.parentNode; 12993 offset = dom.nodeIndex(startNode); 12994 normalized = true; 12995 return; 12996 } 12997 12998 // Walk left until we hit a text node we can move to or a block/br/img 12999 walker = new TreeWalker(startNode, parentBlockContainer); 13000 while ((node = walker[left ? 'prev' : 'next']())) { 13001 // Break if we hit a non content editable node 13002 if (dom.getContentEditableParent(node) === "false") { 13003 return; 13004 } 13005 13006 // Found text node that has a length 13007 if (node.nodeType === 3 && node.nodeValue.length > 0) { 13008 container = node; 13009 offset = left ? node.nodeValue.length : 0; 13010 normalized = true; 13011 return; 13012 } 13013 13014 // Break if we find a block or a BR/IMG/INPUT etc 13015 if (dom.isBlock(node) || nonEmptyElementsMap[node.nodeName.toLowerCase()]) { 13016 return; 13017 } 13018 13019 lastInlineElement = node; 13020 } 13021 13022 // Only fetch the last inline element when in caret mode for now 13023 if (collapsed && lastInlineElement) { 13024 container = lastInlineElement; 13025 normalized = true; 13026 offset = 0; 13027 } 13028 } 13029 13030 container = rng[(start ? 'start' : 'end') + 'Container']; 13031 offset = rng[(start ? 'start' : 'end') + 'Offset']; 13032 isAfterNode = container.nodeType == 1 && offset === container.childNodes.length; 13033 nonEmptyElementsMap = dom.schema.getNonEmptyElements(); 13034 directionLeft = start; 13035 13036 if (container.nodeType == 1 && offset > container.childNodes.length - 1) { 13037 directionLeft = false; 13038 } 13039 13040 // If the container is a document move it to the body element 13041 if (container.nodeType === 9) { 13042 container = dom.getRoot(); 13043 offset = 0; 13044 } 13045 13046 // If the container is body try move it into the closest text node or position 13047 if (container === body) { 13048 // If start is before/after a image, table etc 13049 if (directionLeft) { 13050 node = container.childNodes[offset > 0 ? offset - 1 : 0]; 13051 if (node) { 13052 if (nonEmptyElementsMap[node.nodeName] || node.nodeName == "TABLE") { 13053 return; 13054 } 13055 } 13056 } 13057 13058 // Resolve the index 13059 if (container.hasChildNodes()) { 13060 offset = Math.min(!directionLeft && offset > 0 ? offset - 1 : offset, container.childNodes.length - 1); 13061 container = container.childNodes[offset]; 13062 offset = 0; 13063 13064 // Don't walk into elements that doesn't have any child nodes like a IMG 13065 if (container.hasChildNodes() && !/TABLE/.test(container.nodeName)) { 13066 // Walk the DOM to find a text node to place the caret at or a BR 13067 node = container; 13068 walker = new TreeWalker(container, body); 13069 13070 do { 13071 // Found a text node use that position 13072 if (node.nodeType === 3 && node.nodeValue.length > 0) { 13073 offset = directionLeft ? 0 : node.nodeValue.length; 13074 container = node; 13075 normalized = true; 13076 break; 13077 } 13078 13079 // Found a BR/IMG element that we can place the caret before 13080 if (nonEmptyElementsMap[node.nodeName.toLowerCase()]) { 13081 offset = dom.nodeIndex(node); 13082 container = node.parentNode; 13083 13084 // Put caret after image when moving the end point 13085 if (node.nodeName == "IMG" && !directionLeft) { 13086 offset++; 13087 } 13088 13089 normalized = true; 13090 break; 13091 } 13092 } while ((node = (directionLeft ? walker.next() : walker.prev()))); 13093 } 13094 } 13095 } 13096 13097 // Lean the caret to the left if possible 13098 if (collapsed) { 13099 // So this: <b>x</b><i>|x</i> 13100 // Becomes: <b>x|</b><i>x</i> 13101 // Seems that only gecko has issues with this 13102 if (container.nodeType === 3 && offset === 0) { 13103 findTextNodeRelative(true); 13104 } 13105 13106 // Lean left into empty inline elements when the caret is before a BR 13107 // So this: <i><b></b><i>|<br></i> 13108 // Becomes: <i><b>|</b><i><br></i> 13109 // Seems that only gecko has issues with this. 13110 // Special edge case for <p><a>x</a>|<br></p> since we don't want <p><a>x|</a><br></p> 13111 if (container.nodeType === 1) { 13112 node = container.childNodes[offset]; 13113 13114 // Offset is after the containers last child 13115 // then use the previous child for normalization 13116 if (!node) { 13117 node = container.childNodes[offset - 1]; 13118 } 13119 13120 if (node && node.nodeName === 'BR' && !isPrevNode(node, 'A') && 13121 !hasBrBeforeAfter(node) && !hasBrBeforeAfter(node, true)) { 13122 findTextNodeRelative(true, node); 13123 } 13124 } 13125 } 13126 13127 // Lean the start of the selection right if possible 13128 // So this: x[<b>x]</b> 13129 // Becomes: x<b>[x]</b> 13130 if (directionLeft && !collapsed && container.nodeType === 3 && offset === container.nodeValue.length) { 13131 findTextNodeRelative(false); 13132 } 13133 13134 // Set endpoint if it was normalized 13135 if (normalized) { 13136 rng['set' + (start ? 'Start' : 'End')](container, offset); 13137 } 13138 } 13139 13140 collapsed = rng.collapsed; 13141 13142 normalizeEndPoint(true); 13143 13144 if (!collapsed) { 13145 normalizeEndPoint(); 13146 } 13147 13148 // If it was collapsed then make sure it still is 13149 if (normalized && collapsed) { 13150 rng.collapse(true); 13151 } 13152 13153 return normalized; 13154 }; 13155 } 13156 13157 /** 13158 * Compares two ranges and checks if they are equal. 13159 * 13160 * @static 13161 * @method compareRanges 13162 * @param {DOMRange} rng1 First range to compare. 13163 * @param {DOMRange} rng2 First range to compare. 13164 * @return {Boolean} true/false if the ranges are equal. 13165 */ 13166 RangeUtils.compareRanges = function(rng1, rng2) { 13167 if (rng1 && rng2) { 13168 // Compare native IE ranges 13169 if (rng1.item || rng1.duplicate) { 13170 // Both are control ranges and the selected element matches 13171 if (rng1.item && rng2.item && rng1.item(0) === rng2.item(0)) { 13172 return true; 13173 } 13174 13175 // Both are text ranges and the range matches 13176 if (rng1.isEqual && rng2.isEqual && rng2.isEqual(rng1)) { 13177 return true; 13178 } 13179 } else { 13180 // Compare w3c ranges 13181 return rng1.startContainer == rng2.startContainer && rng1.startOffset == rng2.startOffset; 13182 } 13183 } 13184 13185 return false; 13186 }; 13187 13188 return RangeUtils; 13189 }); 13190 13191 // Included from: js/tinymce/classes/dom/Selection.js 13192 13193 /** 13194 * Selection.js 13195 * 13196 * Copyright, Moxiecode Systems AB 13197 * Released under LGPL License. 13198 * 13199 * License: http://www.tinymce.com/license 13200 * Contributing: http://www.tinymce.com/contributing 13201 */ 13202 13203 /** 13204 * This class handles text and control selection it's an crossbrowser utility class. 13205 * Consult the TinyMCE Wiki API for more details and examples on how to use this class. 13206 * 13207 * @class tinymce.dom.Selection 13208 * @example 13209 * // Getting the currently selected node for the active editor 13210 * alert(tinymce.activeEditor.selection.getNode().nodeName); 13211 */ 13212 define("tinymce/dom/Selection", [ 13213 "tinymce/dom/TreeWalker", 13214 "tinymce/dom/TridentSelection", 13215 "tinymce/dom/ControlSelection", 13216 "tinymce/dom/RangeUtils", 13217 "tinymce/Env", 13218 "tinymce/util/Tools" 13219 ], function(TreeWalker, TridentSelection, ControlSelection, RangeUtils, Env, Tools) { 13220 var each = Tools.each, grep = Tools.grep, trim = Tools.trim; 13221 var isIE = Env.ie, isOpera = Env.opera; 13222 13223 /** 13224 * Constructs a new selection instance. 13225 * 13226 * @constructor 13227 * @method Selection 13228 * @param {tinymce.dom.DOMUtils} dom DOMUtils object reference. 13229 * @param {Window} win Window to bind the selection object to. 13230 * @param {tinymce.dom.Serializer} serializer DOM serialization class to use for getContent. 13231 */ 13232 function Selection(dom, win, serializer, editor) { 13233 var self = this; 13234 13235 self.dom = dom; 13236 self.win = win; 13237 self.serializer = serializer; 13238 self.editor = editor; 13239 13240 self.controlSelection = new ControlSelection(self, editor); 13241 13242 // No W3C Range support 13243 if (!self.win.getSelection) { 13244 self.tridentSel = new TridentSelection(self); 13245 } 13246 } 13247 13248 Selection.prototype = { 13249 /** 13250 * Move the selection cursor range to the specified node and offset. 13251 * If there is no node specified it will move it to the first suitable location within the body. 13252 * 13253 * @method setCursorLocation 13254 * @param {Node} node Optional node to put the cursor in. 13255 * @param {Number} offset Optional offset from the start of the node to put the cursor at. 13256 */ 13257 setCursorLocation: function(node, offset) { 13258 var self = this, rng = self.dom.createRng(); 13259 13260 if (!node) { 13261 self._moveEndPoint(rng, self.editor.getBody(), true); 13262 self.setRng(rng); 13263 } else { 13264 rng.setStart(node, offset); 13265 rng.setEnd(node, offset); 13266 self.setRng(rng); 13267 self.collapse(false); 13268 } 13269 }, 13270 13271 /** 13272 * Returns the selected contents using the DOM serializer passed in to this class. 13273 * 13274 * @method getContent 13275 * @param {Object} s Optional settings class with for example output format text or html. 13276 * @return {String} Selected contents in for example HTML format. 13277 * @example 13278 * // Alerts the currently selected contents 13279 * alert(tinymce.activeEditor.selection.getContent()); 13280 * 13281 * // Alerts the currently selected contents as plain text 13282 * alert(tinymce.activeEditor.selection.getContent({format: 'text'})); 13283 */ 13284 getContent: function(args) { 13285 var self = this, rng = self.getRng(), tmpElm = self.dom.create("body"); 13286 var se = self.getSel(), whiteSpaceBefore, whiteSpaceAfter, fragment; 13287 13288 args = args || {}; 13289 whiteSpaceBefore = whiteSpaceAfter = ''; 13290 args.get = true; 13291 args.format = args.format || 'html'; 13292 args.selection = true; 13293 self.editor.fire('BeforeGetContent', args); 13294 13295 if (args.format == 'text') { 13296 return self.isCollapsed() ? '' : (rng.text || (se.toString ? se.toString() : '')); 13297 } 13298 13299 if (rng.cloneContents) { 13300 fragment = rng.cloneContents(); 13301 13302 if (fragment) { 13303 tmpElm.appendChild(fragment); 13304 } 13305 } else if (rng.item !== undefined || rng.htmlText !== undefined) { 13306 // IE will produce invalid markup if elements are present that 13307 // it doesn't understand like custom elements or HTML5 elements. 13308 // Adding a BR in front of the contents and then remoiving it seems to fix it though. 13309 tmpElm.innerHTML = '<br>' + (rng.item ? rng.item(0).outerHTML : rng.htmlText); 13310 tmpElm.removeChild(tmpElm.firstChild); 13311 } else { 13312 tmpElm.innerHTML = rng.toString(); 13313 } 13314 13315 // Keep whitespace before and after 13316 if (/^\s/.test(tmpElm.innerHTML)) { 13317 whiteSpaceBefore = ' '; 13318 } 13319 13320 if (/\s+$/.test(tmpElm.innerHTML)) { 13321 whiteSpaceAfter = ' '; 13322 } 13323 13324 args.getInner = true; 13325 13326 args.content = self.isCollapsed() ? '' : whiteSpaceBefore + self.serializer.serialize(tmpElm, args) + whiteSpaceAfter; 13327 self.editor.fire('GetContent', args); 13328 13329 return args.content; 13330 }, 13331 13332 /** 13333 * Sets the current selection to the specified content. If any contents is selected it will be replaced 13334 * with the contents passed in to this function. If there is no selection the contents will be inserted 13335 * where the caret is placed in the editor/page. 13336 * 13337 * @method setContent 13338 * @param {String} content HTML contents to set could also be other formats depending on settings. 13339 * @param {Object} args Optional settings object with for example data format. 13340 * @example 13341 * // Inserts some HTML contents at the current selection 13342 * tinymce.activeEditor.selection.setContent('<strong>Some contents</strong>'); 13343 */ 13344 setContent: function(content, args) { 13345 var self = this, rng = self.getRng(), caretNode, doc = self.win.document, frag, temp; 13346 13347 args = args || {format: 'html'}; 13348 args.set = true; 13349 args.selection = true; 13350 content = args.content = content; 13351 13352 // Dispatch before set content event 13353 if (!args.no_events) { 13354 self.editor.fire('BeforeSetContent', args); 13355 } 13356 13357 content = args.content; 13358 13359 if (rng.insertNode) { 13360 // Make caret marker since insertNode places the caret in the beginning of text after insert 13361 content += '<span id="__caret">_</span>'; 13362 13363 // Delete and insert new node 13364 if (rng.startContainer == doc && rng.endContainer == doc) { 13365 // WebKit will fail if the body is empty since the range is then invalid and it can't insert contents 13366 doc.body.innerHTML = content; 13367 } else { 13368 rng.deleteContents(); 13369 13370 if (doc.body.childNodes.length === 0) { 13371 doc.body.innerHTML = content; 13372 } else { 13373 // createContextualFragment doesn't exists in IE 9 DOMRanges 13374 if (rng.createContextualFragment) { 13375 rng.insertNode(rng.createContextualFragment(content)); 13376 } else { 13377 // Fake createContextualFragment call in IE 9 13378 frag = doc.createDocumentFragment(); 13379 temp = doc.createElement('div'); 13380 13381 frag.appendChild(temp); 13382 temp.outerHTML = content; 13383 13384 rng.insertNode(frag); 13385 } 13386 } 13387 } 13388 13389 // Move to caret marker 13390 caretNode = self.dom.get('__caret'); 13391 13392 // Make sure we wrap it compleatly, Opera fails with a simple select call 13393 rng = doc.createRange(); 13394 rng.setStartBefore(caretNode); 13395 rng.setEndBefore(caretNode); 13396 self.setRng(rng); 13397 13398 // Remove the caret position 13399 self.dom.remove('__caret'); 13400 13401 try { 13402 self.setRng(rng); 13403 } catch (ex) { 13404 // Might fail on Opera for some odd reason 13405 } 13406 } else { 13407 if (rng.item) { 13408 // Delete content and get caret text selection 13409 doc.execCommand('Delete', false, null); 13410 rng = self.getRng(); 13411 } 13412 13413 // Explorer removes spaces from the beginning of pasted contents 13414 if (/^\s+/.test(content)) { 13415 rng.pasteHTML('<span id="__mce_tmp">_</span>' + content); 13416 self.dom.remove('__mce_tmp'); 13417 } else { 13418 rng.pasteHTML(content); 13419 } 13420 } 13421 13422 // Dispatch set content event 13423 if (!args.no_events) { 13424 self.editor.fire('SetContent', args); 13425 } 13426 }, 13427 13428 /** 13429 * Returns the start element of a selection range. If the start is in a text 13430 * node the parent element will be returned. 13431 * 13432 * @method getStart 13433 * @return {Element} Start element of selection range. 13434 */ 13435 getStart: function() { 13436 var self = this, rng = self.getRng(), startElement, parentElement, checkRng, node; 13437 13438 if (rng.duplicate || rng.item) { 13439 // Control selection, return first item 13440 if (rng.item) { 13441 return rng.item(0); 13442 } 13443 13444 // Get start element 13445 checkRng = rng.duplicate(); 13446 checkRng.collapse(1); 13447 startElement = checkRng.parentElement(); 13448 if (startElement.ownerDocument !== self.dom.doc) { 13449 startElement = self.dom.getRoot(); 13450 } 13451 13452 // Check if range parent is inside the start element, then return the inner parent element 13453 // This will fix issues when a single element is selected, IE would otherwise return the wrong start element 13454 parentElement = node = rng.parentElement(); 13455 while ((node = node.parentNode)) { 13456 if (node == startElement) { 13457 startElement = parentElement; 13458 break; 13459 } 13460 } 13461 13462 return startElement; 13463 } else { 13464 startElement = rng.startContainer; 13465 13466 if (startElement.nodeType == 1 && startElement.hasChildNodes()) { 13467 startElement = startElement.childNodes[Math.min(startElement.childNodes.length - 1, rng.startOffset)]; 13468 } 13469 13470 if (startElement && startElement.nodeType == 3) { 13471 return startElement.parentNode; 13472 } 13473 13474 return startElement; 13475 } 13476 }, 13477 13478 /** 13479 * Returns the end element of a selection range. If the end is in a text 13480 * node the parent element will be returned. 13481 * 13482 * @method getEnd 13483 * @return {Element} End element of selection range. 13484 */ 13485 getEnd: function() { 13486 var self = this, rng = self.getRng(), endElement, endOffset; 13487 13488 if (rng.duplicate || rng.item) { 13489 if (rng.item) { 13490 return rng.item(0); 13491 } 13492 13493 rng = rng.duplicate(); 13494 rng.collapse(0); 13495 endElement = rng.parentElement(); 13496 if (endElement.ownerDocument !== self.dom.doc) { 13497 endElement = self.dom.getRoot(); 13498 } 13499 13500 if (endElement && endElement.nodeName == 'BODY') { 13501 return endElement.lastChild || endElement; 13502 } 13503 13504 return endElement; 13505 } else { 13506 endElement = rng.endContainer; 13507 endOffset = rng.endOffset; 13508 13509 if (endElement.nodeType == 1 && endElement.hasChildNodes()) { 13510 endElement = endElement.childNodes[endOffset > 0 ? endOffset - 1 : endOffset]; 13511 } 13512 13513 if (endElement && endElement.nodeType == 3) { 13514 return endElement.parentNode; 13515 } 13516 13517 return endElement; 13518 } 13519 }, 13520 13521 /** 13522 * Returns a bookmark location for the current selection. This bookmark object 13523 * can then be used to restore the selection after some content modification to the document. 13524 * 13525 * @method getBookmark 13526 * @param {Number} type Optional state if the bookmark should be simple or not. Default is complex. 13527 * @param {Boolean} normalized Optional state that enables you to get a position that it would be after normalization. 13528 * @return {Object} Bookmark object, use moveToBookmark with this object to restore the selection. 13529 * @example 13530 * // Stores a bookmark of the current selection 13531 * var bm = tinymce.activeEditor.selection.getBookmark(); 13532 * 13533 * tinymce.activeEditor.setContent(tinymce.activeEditor.getContent() + 'Some new content'); 13534 * 13535 * // Restore the selection bookmark 13536 * tinymce.activeEditor.selection.moveToBookmark(bm); 13537 */ 13538 getBookmark: function(type, normalized) { 13539 var self = this, dom = self.dom, rng, rng2, id, collapsed, name, element, chr = '', styles; 13540 13541 function findIndex(name, element) { 13542 var index = 0; 13543 13544 each(dom.select(name), function(node, i) { 13545 if (node == element) { 13546 index = i; 13547 } 13548 }); 13549 13550 return index; 13551 } 13552 13553 function normalizeTableCellSelection(rng) { 13554 function moveEndPoint(start) { 13555 var container, offset, childNodes, prefix = start ? 'start' : 'end'; 13556 13557 container = rng[prefix + 'Container']; 13558 offset = rng[prefix + 'Offset']; 13559 13560 if (container.nodeType == 1 && container.nodeName == "TR") { 13561 childNodes = container.childNodes; 13562 container = childNodes[Math.min(start ? offset : offset - 1, childNodes.length - 1)]; 13563 if (container) { 13564 offset = start ? 0 : container.childNodes.length; 13565 rng['set' + (start ? 'Start' : 'End')](container, offset); 13566 } 13567 } 13568 } 13569 13570 moveEndPoint(true); 13571 moveEndPoint(); 13572 13573 return rng; 13574 } 13575 13576 function getLocation() { 13577 var rng = self.getRng(true), root = dom.getRoot(), bookmark = {}; 13578 13579 function getPoint(rng, start) { 13580 var container = rng[start ? 'startContainer' : 'endContainer'], 13581 offset = rng[start ? 'startOffset' : 'endOffset'], point = [], node, childNodes, after = 0; 13582 13583 if (container.nodeType == 3) { 13584 if (normalized) { 13585 for (node = container.previousSibling; node && node.nodeType == 3; node = node.previousSibling) { 13586 offset += node.nodeValue.length; 13587 } 13588 } 13589 13590 point.push(offset); 13591 } else { 13592 childNodes = container.childNodes; 13593 13594 if (offset >= childNodes.length && childNodes.length) { 13595 after = 1; 13596 offset = Math.max(0, childNodes.length - 1); 13597 } 13598 13599 point.push(self.dom.nodeIndex(childNodes[offset], normalized) + after); 13600 } 13601 13602 for (; container && container != root; container = container.parentNode) { 13603 point.push(self.dom.nodeIndex(container, normalized)); 13604 } 13605 13606 return point; 13607 } 13608 13609 bookmark.start = getPoint(rng, true); 13610 13611 if (!self.isCollapsed()) { 13612 bookmark.end = getPoint(rng); 13613 } 13614 13615 return bookmark; 13616 } 13617 13618 if (type == 2) { 13619 element = self.getNode(); 13620 name = element ? element.nodeName : null; 13621 13622 if (name == 'IMG') { 13623 return {name: name, index: findIndex(name, element)}; 13624 } 13625 13626 if (self.tridentSel) { 13627 return self.tridentSel.getBookmark(type); 13628 } 13629 13630 return getLocation(); 13631 } 13632 13633 // Handle simple range 13634 if (type) { 13635 return {rng: self.getRng()}; 13636 } 13637 13638 rng = self.getRng(); 13639 id = dom.uniqueId(); 13640 collapsed = self.isCollapsed(); 13641 styles = 'overflow:hidden;line-height:0px'; 13642 13643 // Explorer method 13644 if (rng.duplicate || rng.item) { 13645 // Text selection 13646 if (!rng.item) { 13647 rng2 = rng.duplicate(); 13648 13649 try { 13650 // Insert start marker 13651 rng.collapse(); 13652 rng.pasteHTML('<span data-mce-type="bookmark" id="' + id + '_start" style="' + styles + '">' + chr + '</span>'); 13653 13654 // Insert end marker 13655 if (!collapsed) { 13656 rng2.collapse(false); 13657 13658 // Detect the empty space after block elements in IE and move the 13659 // end back one character <p></p>] becomes <p>]</p> 13660 rng.moveToElementText(rng2.parentElement()); 13661 if (rng.compareEndPoints('StartToEnd', rng2) === 0) { 13662 rng2.move('character', -1); 13663 } 13664 13665 rng2.pasteHTML('<span data-mce-type="bookmark" id="' + id + '_end" style="' + styles + '">' + chr + '</span>'); 13666 } 13667 } catch (ex) { 13668 // IE might throw unspecified error so lets ignore it 13669 return null; 13670 } 13671 } else { 13672 // Control selection 13673 element = rng.item(0); 13674 name = element.nodeName; 13675 13676 return {name: name, index: findIndex(name, element)}; 13677 } 13678 } else { 13679 element = self.getNode(); 13680 name = element.nodeName; 13681 if (name == 'IMG') { 13682 return {name: name, index: findIndex(name, element)}; 13683 } 13684 13685 // W3C method 13686 rng2 = normalizeTableCellSelection(rng.cloneRange()); 13687 13688 // Insert end marker 13689 if (!collapsed) { 13690 rng2.collapse(false); 13691 rng2.insertNode(dom.create('span', {'data-mce-type': "bookmark", id: id + '_end', style: styles}, chr)); 13692 } 13693 13694 rng = normalizeTableCellSelection(rng); 13695 rng.collapse(true); 13696 rng.insertNode(dom.create('span', {'data-mce-type': "bookmark", id: id + '_start', style: styles}, chr)); 13697 } 13698 13699 self.moveToBookmark({id: id, keep: 1}); 13700 13701 return {id: id}; 13702 }, 13703 13704 /** 13705 * Restores the selection to the specified bookmark. 13706 * 13707 * @method moveToBookmark 13708 * @param {Object} bookmark Bookmark to restore selection from. 13709 * @return {Boolean} true/false if it was successful or not. 13710 * @example 13711 * // Stores a bookmark of the current selection 13712 * var bm = tinymce.activeEditor.selection.getBookmark(); 13713 * 13714 * tinymce.activeEditor.setContent(tinymce.activeEditor.getContent() + 'Some new content'); 13715 * 13716 * // Restore the selection bookmark 13717 * tinymce.activeEditor.selection.moveToBookmark(bm); 13718 */ 13719 moveToBookmark: function(bookmark) { 13720 var self = this, dom = self.dom, rng, root, startContainer, endContainer, startOffset, endOffset; 13721 13722 function setEndPoint(start) { 13723 var point = bookmark[start ? 'start' : 'end'], i, node, offset, children; 13724 13725 if (point) { 13726 offset = point[0]; 13727 13728 // Find container node 13729 for (node = root, i = point.length - 1; i >= 1; i--) { 13730 children = node.childNodes; 13731 13732 if (point[i] > children.length - 1) { 13733 return; 13734 } 13735 13736 node = children[point[i]]; 13737 } 13738 13739 // Move text offset to best suitable location 13740 if (node.nodeType === 3) { 13741 offset = Math.min(point[0], node.nodeValue.length); 13742 } 13743 13744 // Move element offset to best suitable location 13745 if (node.nodeType === 1) { 13746 offset = Math.min(point[0], node.childNodes.length); 13747 } 13748 13749 // Set offset within container node 13750 if (start) { 13751 rng.setStart(node, offset); 13752 } else { 13753 rng.setEnd(node, offset); 13754 } 13755 } 13756 13757 return true; 13758 } 13759 13760 function restoreEndPoint(suffix) { 13761 var marker = dom.get(bookmark.id + '_' + suffix), node, idx, next, prev, keep = bookmark.keep; 13762 13763 if (marker) { 13764 node = marker.parentNode; 13765 13766 if (suffix == 'start') { 13767 if (!keep) { 13768 idx = dom.nodeIndex(marker); 13769 } else { 13770 node = marker.firstChild; 13771 idx = 1; 13772 } 13773 13774 startContainer = endContainer = node; 13775 startOffset = endOffset = idx; 13776 } else { 13777 if (!keep) { 13778 idx = dom.nodeIndex(marker); 13779 } else { 13780 node = marker.firstChild; 13781 idx = 1; 13782 } 13783 13784 endContainer = node; 13785 endOffset = idx; 13786 } 13787 13788 if (!keep) { 13789 prev = marker.previousSibling; 13790 next = marker.nextSibling; 13791 13792 // Remove all marker text nodes 13793 each(grep(marker.childNodes), function(node) { 13794 if (node.nodeType == 3) { 13795 node.nodeValue = node.nodeValue.replace(/\uFEFF/g, ''); 13796 } 13797 }); 13798 13799 // Remove marker but keep children if for example contents where inserted into the marker 13800 // Also remove duplicated instances of the marker for example by a 13801 // split operation or by WebKit auto split on paste feature 13802 while ((marker = dom.get(bookmark.id + '_' + suffix))) { 13803 dom.remove(marker, 1); 13804 } 13805 13806 // If siblings are text nodes then merge them unless it's Opera since it some how removes the node 13807 // and we are sniffing since adding a lot of detection code for a browser with 3% of the market 13808 // isn't worth the effort. Sorry, Opera but it's just a fact 13809 if (prev && next && prev.nodeType == next.nodeType && prev.nodeType == 3 && !isOpera) { 13810 idx = prev.nodeValue.length; 13811 prev.appendData(next.nodeValue); 13812 dom.remove(next); 13813 13814 if (suffix == 'start') { 13815 startContainer = endContainer = prev; 13816 startOffset = endOffset = idx; 13817 } else { 13818 endContainer = prev; 13819 endOffset = idx; 13820 } 13821 } 13822 } 13823 } 13824 } 13825 13826 function addBogus(node) { 13827 // Adds a bogus BR element for empty block elements 13828 if (dom.isBlock(node) && !node.innerHTML && !isIE) { 13829 node.innerHTML = '<br data-mce-bogus="1" />'; 13830 } 13831 13832 return node; 13833 } 13834 13835 if (bookmark) { 13836 if (bookmark.start) { 13837 rng = dom.createRng(); 13838 root = dom.getRoot(); 13839 13840 if (self.tridentSel) { 13841 return self.tridentSel.moveToBookmark(bookmark); 13842 } 13843 13844 if (setEndPoint(true) && setEndPoint()) { 13845 self.setRng(rng); 13846 } 13847 } else if (bookmark.id) { 13848 // Restore start/end points 13849 restoreEndPoint('start'); 13850 restoreEndPoint('end'); 13851 13852 if (startContainer) { 13853 rng = dom.createRng(); 13854 rng.setStart(addBogus(startContainer), startOffset); 13855 rng.setEnd(addBogus(endContainer), endOffset); 13856 self.setRng(rng); 13857 } 13858 } else if (bookmark.name) { 13859 self.select(dom.select(bookmark.name)[bookmark.index]); 13860 } else if (bookmark.rng) { 13861 self.setRng(bookmark.rng); 13862 } 13863 } 13864 }, 13865 13866 /** 13867 * Selects the specified element. This will place the start and end of the selection range around the element. 13868 * 13869 * @method select 13870 * @param {Element} node HMTL DOM element to select. 13871 * @param {Boolean} content Optional bool state if the contents should be selected or not on non IE browser. 13872 * @return {Element} Selected element the same element as the one that got passed in. 13873 * @example 13874 * // Select the first paragraph in the active editor 13875 * tinymce.activeEditor.selection.select(tinymce.activeEditor.dom.select('p')[0]); 13876 */ 13877 select: function(node, content) { 13878 var self = this, dom = self.dom, rng = dom.createRng(), idx; 13879 13880 // Clear stored range set by FocusManager 13881 self.lastFocusBookmark = null; 13882 13883 if (node) { 13884 if (!content && self.controlSelection.controlSelect(node)) { 13885 return; 13886 } 13887 13888 idx = dom.nodeIndex(node); 13889 rng.setStart(node.parentNode, idx); 13890 rng.setEnd(node.parentNode, idx + 1); 13891 13892 // Find first/last text node or BR element 13893 if (content) { 13894 self._moveEndPoint(rng, node, true); 13895 self._moveEndPoint(rng, node); 13896 } 13897 13898 self.setRng(rng); 13899 } 13900 13901 return node; 13902 }, 13903 13904 /** 13905 * Returns true/false if the selection range is collapsed or not. Collapsed means if it's a caret or a larger selection. 13906 * 13907 * @method isCollapsed 13908 * @return {Boolean} true/false state if the selection range is collapsed or not. 13909 * Collapsed means if it's a caret or a larger selection. 13910 */ 13911 isCollapsed: function() { 13912 var self = this, rng = self.getRng(), sel = self.getSel(); 13913 13914 if (!rng || rng.item) { 13915 return false; 13916 } 13917 13918 if (rng.compareEndPoints) { 13919 return rng.compareEndPoints('StartToEnd', rng) === 0; 13920 } 13921 13922 return !sel || rng.collapsed; 13923 }, 13924 13925 /** 13926 * Collapse the selection to start or end of range. 13927 * 13928 * @method collapse 13929 * @param {Boolean} to_start Optional boolean state if to collapse to end or not. Defaults to start. 13930 */ 13931 collapse: function(to_start) { 13932 var self = this, rng = self.getRng(), node; 13933 13934 // Control range on IE 13935 if (rng.item) { 13936 node = rng.item(0); 13937 rng = self.win.document.body.createTextRange(); 13938 rng.moveToElementText(node); 13939 } 13940 13941 rng.collapse(!!to_start); 13942 self.setRng(rng); 13943 }, 13944 13945 /** 13946 * Returns the browsers internal selection object. 13947 * 13948 * @method getSel 13949 * @return {Selection} Internal browser selection object. 13950 */ 13951 getSel: function() { 13952 var win = this.win; 13953 13954 return win.getSelection ? win.getSelection() : win.document.selection; 13955 }, 13956 13957 /** 13958 * Returns the browsers internal range object. 13959 * 13960 * @method getRng 13961 * @param {Boolean} w3c Forces a compatible W3C range on IE. 13962 * @return {Range} Internal browser range object. 13963 * @see http://www.quirksmode.org/dom/range_intro.html 13964 * @see http://www.dotvoid.com/2001/03/using-the-range-object-in-mozilla/ 13965 */ 13966 getRng: function(w3c) { 13967 var self = this, selection, rng, elm, doc = self.win.document, ieRng; 13968 13969 function tryCompareBounderyPoints(how, sourceRange, destinationRange) { 13970 try { 13971 return sourceRange.compareBoundaryPoints(how, destinationRange); 13972 } catch (ex) { 13973 // Gecko throws wrong document exception if the range points 13974 // to nodes that where removed from the dom #6690 13975 // Browsers should mutate existing DOMRange instances so that they always point 13976 // to something in the document this is not the case in Gecko works fine in IE/WebKit/Blink 13977 // For performance reasons just return -1 13978 return -1; 13979 } 13980 } 13981 13982 // Use last rng passed from FocusManager if it's available this enables 13983 // calls to editor.selection.getStart() to work when caret focus is lost on IE 13984 if (!w3c && self.lastFocusBookmark) { 13985 var bookmark = self.lastFocusBookmark; 13986 13987 // Convert bookmark to range IE 11 fix 13988 if (bookmark.startContainer) { 13989 rng = doc.createRange(); 13990 rng.setStart(bookmark.startContainer, bookmark.startOffset); 13991 rng.setEnd(bookmark.endContainer, bookmark.endOffset); 13992 } else { 13993 rng = bookmark; 13994 } 13995 13996 return rng; 13997 } 13998 13999 // Found tridentSel object then we need to use that one 14000 if (w3c && self.tridentSel) { 14001 return self.tridentSel.getRangeAt(0); 14002 } 14003 14004 try { 14005 if ((selection = self.getSel())) { 14006 if (selection.rangeCount > 0) { 14007 rng = selection.getRangeAt(0); 14008 } else { 14009 rng = selection.createRange ? selection.createRange() : doc.createRange(); 14010 } 14011 } 14012 } catch (ex) { 14013 // IE throws unspecified error here if TinyMCE is placed in a frame/iframe 14014 } 14015 14016 // We have W3C ranges and it's IE then fake control selection since IE9 doesn't handle that correctly yet 14017 // IE 11 doesn't support the selection object so we check for that as well 14018 if (isIE && rng && rng.setStart && doc.selection) { 14019 try { 14020 // IE will sometimes throw an exception here 14021 ieRng = doc.selection.createRange(); 14022 } catch (ex) { 14023 14024 } 14025 14026 if (ieRng && ieRng.item) { 14027 elm = ieRng.item(0); 14028 rng = doc.createRange(); 14029 rng.setStartBefore(elm); 14030 rng.setEndAfter(elm); 14031 } 14032 } 14033 14034 // No range found then create an empty one 14035 // This can occur when the editor is placed in a hidden container element on Gecko 14036 // Or on IE when there was an exception 14037 if (!rng) { 14038 rng = doc.createRange ? doc.createRange() : doc.body.createTextRange(); 14039 } 14040 14041 // If range is at start of document then move it to start of body 14042 if (rng.setStart && rng.startContainer.nodeType === 9 && rng.collapsed) { 14043 elm = self.dom.getRoot(); 14044 rng.setStart(elm, 0); 14045 rng.setEnd(elm, 0); 14046 } 14047 14048 if (self.selectedRange && self.explicitRange) { 14049 if (tryCompareBounderyPoints(rng.START_TO_START, rng, self.selectedRange) === 0 && 14050 tryCompareBounderyPoints(rng.END_TO_END, rng, self.selectedRange) === 0) { 14051 // Safari, Opera and Chrome only ever select text which causes the range to change. 14052 // This lets us use the originally set range if the selection hasn't been changed by the user. 14053 rng = self.explicitRange; 14054 } else { 14055 self.selectedRange = null; 14056 self.explicitRange = null; 14057 } 14058 } 14059 14060 return rng; 14061 }, 14062 14063 /** 14064 * Changes the selection to the specified DOM range. 14065 * 14066 * @method setRng 14067 * @param {Range} rng Range to select. 14068 */ 14069 setRng: function(rng, forward) { 14070 var self = this, sel; 14071 14072 // Is IE specific range 14073 if (rng.select) { 14074 try { 14075 rng.select(); 14076 } catch (ex) { 14077 // Needed for some odd IE bug #1843306 14078 } 14079 14080 return; 14081 } 14082 14083 if (!self.tridentSel) { 14084 sel = self.getSel(); 14085 14086 if (sel) { 14087 self.explicitRange = rng; 14088 14089 try { 14090 sel.removeAllRanges(); 14091 sel.addRange(rng); 14092 } catch (ex) { 14093 // IE might throw errors here if the editor is within a hidden container and selection is changed 14094 } 14095 14096 // Forward is set to false and we have an extend function 14097 if (forward === false && sel.extend) { 14098 sel.collapse(rng.endContainer, rng.endOffset); 14099 sel.extend(rng.startContainer, rng.startOffset); 14100 } 14101 14102 // adding range isn't always successful so we need to check range count otherwise an exception can occur 14103 self.selectedRange = sel.rangeCount > 0 ? sel.getRangeAt(0) : null; 14104 } 14105 } else { 14106 // Is W3C Range fake range on IE 14107 if (rng.cloneRange) { 14108 try { 14109 self.tridentSel.addRange(rng); 14110 return; 14111 } catch (ex) { 14112 //IE9 throws an error here if called before selection is placed in the editor 14113 } 14114 } 14115 } 14116 }, 14117 14118 /** 14119 * Sets the current selection to the specified DOM element. 14120 * 14121 * @method setNode 14122 * @param {Element} elm Element to set as the contents of the selection. 14123 * @return {Element} Returns the element that got passed in. 14124 * @example 14125 * // Inserts a DOM node at current selection/caret location 14126 * tinymce.activeEditor.selection.setNode(tinymce.activeEditor.dom.create('img', {src: 'some.gif', title: 'some title'})); 14127 */ 14128 setNode: function(elm) { 14129 var self = this; 14130 14131 self.setContent(self.dom.getOuterHTML(elm)); 14132 14133 return elm; 14134 }, 14135 14136 /** 14137 * Returns the currently selected element or the common ancestor element for both start and end of the selection. 14138 * 14139 * @method getNode 14140 * @return {Element} Currently selected element or common ancestor element. 14141 * @example 14142 * // Alerts the currently selected elements node name 14143 * alert(tinymce.activeEditor.selection.getNode().nodeName); 14144 */ 14145 getNode: function() { 14146 var self = this, rng = self.getRng(), elm; 14147 var startContainer = rng.startContainer, endContainer = rng.endContainer; 14148 var startOffset = rng.startOffset, endOffset = rng.endOffset, root = self.dom.getRoot(); 14149 14150 function skipEmptyTextNodes(node, forwards) { 14151 var orig = node; 14152 14153 while (node && node.nodeType === 3 && node.length === 0) { 14154 node = forwards ? node.nextSibling : node.previousSibling; 14155 } 14156 14157 return node || orig; 14158 } 14159 14160 // Range maybe lost after the editor is made visible again 14161 if (!rng) { 14162 return root; 14163 } 14164 14165 if (rng.setStart) { 14166 elm = rng.commonAncestorContainer; 14167 14168 // Handle selection a image or other control like element such as anchors 14169 if (!rng.collapsed) { 14170 if (startContainer == endContainer) { 14171 if (endOffset - startOffset < 2) { 14172 if (startContainer.hasChildNodes()) { 14173 elm = startContainer.childNodes[startOffset]; 14174 } 14175 } 14176 } 14177 14178 // If the anchor node is a element instead of a text node then return this element 14179 //if (tinymce.isWebKit && sel.anchorNode && sel.anchorNode.nodeType == 1) 14180 // return sel.anchorNode.childNodes[sel.anchorOffset]; 14181 14182 // Handle cases where the selection is immediately wrapped around a node and return that node instead of it's parent. 14183 // This happens when you double click an underlined word in FireFox. 14184 if (startContainer.nodeType === 3 && endContainer.nodeType === 3) { 14185 if (startContainer.length === startOffset) { 14186 startContainer = skipEmptyTextNodes(startContainer.nextSibling, true); 14187 } else { 14188 startContainer = startContainer.parentNode; 14189 } 14190 14191 if (endOffset === 0) { 14192 endContainer = skipEmptyTextNodes(endContainer.previousSibling, false); 14193 } else { 14194 endContainer = endContainer.parentNode; 14195 } 14196 14197 if (startContainer && startContainer === endContainer) { 14198 return startContainer; 14199 } 14200 } 14201 } 14202 14203 if (elm && elm.nodeType == 3) { 14204 return elm.parentNode; 14205 } 14206 14207 return elm; 14208 } 14209 14210 elm = rng.item ? rng.item(0) : rng.parentElement(); 14211 14212 // IE 7 might return elements outside the iframe 14213 if (elm.ownerDocument !== self.win.document) { 14214 elm = root; 14215 } 14216 14217 return elm; 14218 }, 14219 14220 getSelectedBlocks: function(startElm, endElm) { 14221 var self = this, dom = self.dom, node, root, selectedBlocks = []; 14222 14223 root = dom.getRoot(); 14224 startElm = dom.getParent(startElm || self.getStart(), dom.isBlock); 14225 endElm = dom.getParent(endElm || self.getEnd(), dom.isBlock); 14226 14227 if (startElm && startElm != root) { 14228 selectedBlocks.push(startElm); 14229 } 14230 14231 if (startElm && endElm && startElm != endElm) { 14232 node = startElm; 14233 14234 var walker = new TreeWalker(startElm, root); 14235 while ((node = walker.next()) && node != endElm) { 14236 if (dom.isBlock(node)) { 14237 selectedBlocks.push(node); 14238 } 14239 } 14240 } 14241 14242 if (endElm && startElm != endElm && endElm != root) { 14243 selectedBlocks.push(endElm); 14244 } 14245 14246 return selectedBlocks; 14247 }, 14248 14249 isForward: function() { 14250 var dom = this.dom, sel = this.getSel(), anchorRange, focusRange; 14251 14252 // No support for selection direction then always return true 14253 if (!sel || !sel.anchorNode || !sel.focusNode) { 14254 return true; 14255 } 14256 14257 anchorRange = dom.createRng(); 14258 anchorRange.setStart(sel.anchorNode, sel.anchorOffset); 14259 anchorRange.collapse(true); 14260 14261 focusRange = dom.createRng(); 14262 focusRange.setStart(sel.focusNode, sel.focusOffset); 14263 focusRange.collapse(true); 14264 14265 return anchorRange.compareBoundaryPoints(anchorRange.START_TO_START, focusRange) <= 0; 14266 }, 14267 14268 normalize: function() { 14269 var self = this, rng = self.getRng(); 14270 14271 if (!isIE && new RangeUtils(self.dom).normalize(rng)) { 14272 self.setRng(rng, self.isForward()); 14273 } 14274 14275 return rng; 14276 }, 14277 14278 /** 14279 * Executes callback of the current selection matches the specified selector or not and passes the state and args to the callback. 14280 * 14281 * @method selectorChanged 14282 * @param {String} selector CSS selector to check for. 14283 * @param {function} callback Callback with state and args when the selector is matches or not. 14284 */ 14285 selectorChanged: function(selector, callback) { 14286 var self = this, currentSelectors; 14287 14288 if (!self.selectorChangedData) { 14289 self.selectorChangedData = {}; 14290 currentSelectors = {}; 14291 14292 self.editor.on('NodeChange', function(e) { 14293 var node = e.element, dom = self.dom, parents = dom.getParents(node, null, dom.getRoot()), matchedSelectors = {}; 14294 14295 // Check for new matching selectors 14296 each(self.selectorChangedData, function(callbacks, selector) { 14297 each(parents, function(node) { 14298 if (dom.is(node, selector)) { 14299 if (!currentSelectors[selector]) { 14300 // Execute callbacks 14301 each(callbacks, function(callback) { 14302 callback(true, {node: node, selector: selector, parents: parents}); 14303 }); 14304 14305 currentSelectors[selector] = callbacks; 14306 } 14307 14308 matchedSelectors[selector] = callbacks; 14309 return false; 14310 } 14311 }); 14312 }); 14313 14314 // Check if current selectors still match 14315 each(currentSelectors, function(callbacks, selector) { 14316 if (!matchedSelectors[selector]) { 14317 delete currentSelectors[selector]; 14318 14319 each(callbacks, function(callback) { 14320 callback(false, {node: node, selector: selector, parents: parents}); 14321 }); 14322 } 14323 }); 14324 }); 14325 } 14326 14327 // Add selector listeners 14328 if (!self.selectorChangedData[selector]) { 14329 self.selectorChangedData[selector] = []; 14330 } 14331 14332 self.selectorChangedData[selector].push(callback); 14333 14334 return self; 14335 }, 14336 14337 getScrollContainer: function() { 14338 var scrollContainer, node = this.dom.getRoot(); 14339 14340 while (node && node.nodeName != 'BODY') { 14341 if (node.scrollHeight > node.clientHeight) { 14342 scrollContainer = node; 14343 break; 14344 } 14345 14346 node = node.parentNode; 14347 } 14348 14349 return scrollContainer; 14350 }, 14351 14352 scrollIntoView: function(elm) { 14353 var y, viewPort, self = this, dom = self.dom, root = dom.getRoot(), viewPortY, viewPortH; 14354 14355 function getPos(elm) { 14356 var x = 0, y = 0; 14357 14358 var offsetParent = elm; 14359 while (offsetParent && offsetParent.nodeType) { 14360 x += offsetParent.offsetLeft || 0; 14361 y += offsetParent.offsetTop || 0; 14362 offsetParent = offsetParent.offsetParent; 14363 } 14364 14365 return {x: x, y: y}; 14366 } 14367 14368 if (root.nodeName != 'BODY') { 14369 var scrollContainer = self.getScrollContainer(); 14370 if (scrollContainer) { 14371 y = getPos(elm).y - getPos(scrollContainer).y; 14372 viewPortH = scrollContainer.clientHeight; 14373 viewPortY = scrollContainer.scrollTop; 14374 if (y < viewPortY || y + 25 > viewPortY + viewPortH) { 14375 scrollContainer.scrollTop = y < viewPortY ? y : y - viewPortH + 25; 14376 } 14377 14378 return; 14379 } 14380 } 14381 14382 viewPort = dom.getViewPort(self.editor.getWin()); 14383 y = dom.getPos(elm).y; 14384 viewPortY = viewPort.y; 14385 viewPortH = viewPort.h; 14386 if (y < viewPort.y || y + 25 > viewPortY + viewPortH) { 14387 self.editor.getWin().scrollTo(0, y < viewPortY ? y : y - viewPortH + 25); 14388 } 14389 }, 14390 14391 _moveEndPoint: function(rng, node, start) { 14392 var root = node, walker = new TreeWalker(node, root); 14393 var nonEmptyElementsMap = this.dom.schema.getNonEmptyElements(); 14394 14395 do { 14396 // Text node 14397 if (node.nodeType == 3 && trim(node.nodeValue).length !== 0) { 14398 if (start) { 14399 rng.setStart(node, 0); 14400 } else { 14401 rng.setEnd(node, node.nodeValue.length); 14402 } 14403 14404 return; 14405 } 14406 14407 // BR/IMG/INPUT elements 14408 if (nonEmptyElementsMap[node.nodeName]) { 14409 if (start) { 14410 rng.setStartBefore(node); 14411 } else { 14412 if (node.nodeName == 'BR') { 14413 rng.setEndBefore(node); 14414 } else { 14415 rng.setEndAfter(node); 14416 } 14417 } 14418 14419 return; 14420 } 14421 14422 // Found empty text block old IE can place the selection inside those 14423 if (Env.ie && Env.ie < 11 && this.dom.isBlock(node) && this.dom.isEmpty(node)) { 14424 if (start) { 14425 rng.setStart(node, 0); 14426 } else { 14427 rng.setEnd(node, 0); 14428 } 14429 14430 return; 14431 } 14432 } while ((node = (start ? walker.next() : walker.prev()))); 14433 14434 // Failed to find any text node or other suitable location then move to the root of body 14435 if (root.nodeName == 'BODY') { 14436 if (start) { 14437 rng.setStart(root, 0); 14438 } else { 14439 rng.setEnd(root, root.childNodes.length); 14440 } 14441 } 14442 }, 14443 14444 destroy: function() { 14445 this.win = null; 14446 this.controlSelection.destroy(); 14447 } 14448 }; 14449 14450 return Selection; 14451 }); 14452 14453 // Included from: js/tinymce/classes/fmt/Preview.js 14454 14455 /** 14456 * Preview.js 14457 * 14458 * Copyright, Moxiecode Systems AB 14459 * Released under LGPL License. 14460 * 14461 * License: http://www.tinymce.com/license 14462 * Contributing: http://www.tinymce.com/contributing 14463 */ 14464 14465 /** 14466 * Internal class for generating previews styles for formats. 14467 * 14468 * Example: 14469 * Preview.getCssText(editor, 'bold'); 14470 * 14471 * @class tinymce.fmt.Preview 14472 * @private 14473 */ 14474 define("tinymce/fmt/Preview", [ 14475 "tinymce/util/Tools" 14476 ], function(Tools) { 14477 var each = Tools.each; 14478 14479 function getCssText(editor, format) { 14480 var name, previewElm, dom = editor.dom; 14481 var previewCss = '', parentFontSize, previewStyles; 14482 14483 previewStyles = editor.settings.preview_styles; 14484 14485 // No preview forced 14486 if (previewStyles === false) { 14487 return ''; 14488 } 14489 14490 // Default preview 14491 if (!previewStyles) { 14492 previewStyles = 'font-family font-size font-weight font-style text-decoration ' + 14493 'text-transform color background-color border border-radius outline text-shadow'; 14494 } 14495 14496 // Removes any variables since these can't be previewed 14497 function removeVars(val) { 14498 return val.replace(/%(\w+)/g, ''); 14499 } 14500 14501 // Create block/inline element to use for preview 14502 if (typeof(format) == "string") { 14503 format = editor.formatter.get(format); 14504 if (!format) { 14505 return; 14506 } 14507 14508 format = format[0]; 14509 } 14510 14511 name = format.block || format.inline || 'span'; 14512 previewElm = dom.create(name); 14513 14514 // Add format styles to preview element 14515 each(format.styles, function(value, name) { 14516 value = removeVars(value); 14517 14518 if (value) { 14519 dom.setStyle(previewElm, name, value); 14520 } 14521 }); 14522 14523 // Add attributes to preview element 14524 each(format.attributes, function(value, name) { 14525 value = removeVars(value); 14526 14527 if (value) { 14528 dom.setAttrib(previewElm, name, value); 14529 } 14530 }); 14531 14532 // Add classes to preview element 14533 each(format.classes, function(value) { 14534 value = removeVars(value); 14535 14536 if (!dom.hasClass(previewElm, value)) { 14537 dom.addClass(previewElm, value); 14538 } 14539 }); 14540 14541 editor.fire('PreviewFormats'); 14542 14543 // Add the previewElm outside the visual area 14544 dom.setStyles(previewElm, {position: 'absolute', left: -0xFFFF}); 14545 editor.getBody().appendChild(previewElm); 14546 14547 // Get parent container font size so we can compute px values out of em/% for older IE:s 14548 parentFontSize = dom.getStyle(editor.getBody(), 'fontSize', true); 14549 parentFontSize = /px$/.test(parentFontSize) ? parseInt(parentFontSize, 10) : 0; 14550 14551 each(previewStyles.split(' '), function(name) { 14552 var value = dom.getStyle(previewElm, name, true); 14553 14554 // If background is transparent then check if the body has a background color we can use 14555 if (name == 'background-color' && /transparent|rgba\s*\([^)]+,\s*0\)/.test(value)) { 14556 value = dom.getStyle(editor.getBody(), name, true); 14557 14558 // Ignore white since it's the default color, not the nicest fix 14559 // TODO: Fix this by detecting runtime style 14560 if (dom.toHex(value).toLowerCase() == '#ffffff') { 14561 return; 14562 } 14563 } 14564 14565 if (name == 'color') { 14566 // Ignore black since it's the default color, not the nicest fix 14567 // TODO: Fix this by detecting runtime style 14568 if (dom.toHex(value).toLowerCase() == '#000000') { 14569 return; 14570 } 14571 } 14572 14573 // Old IE won't calculate the font size so we need to do that manually 14574 if (name == 'font-size') { 14575 if (/em|%$/.test(value)) { 14576 if (parentFontSize === 0) { 14577 return; 14578 } 14579 14580 // Convert font size from em/% to px 14581 value = parseFloat(value, 10) / (/%$/.test(value) ? 100 : 1); 14582 value = (value * parentFontSize) + 'px'; 14583 } 14584 } 14585 14586 if (name == "border" && value) { 14587 previewCss += 'padding:0 2px;'; 14588 } 14589 14590 previewCss += name + ':' + value + ';'; 14591 }); 14592 14593 editor.fire('AfterPreviewFormats'); 14594 14595 //previewCss += 'line-height:normal'; 14596 14597 dom.remove(previewElm); 14598 14599 return previewCss; 14600 } 14601 14602 return { 14603 getCssText: getCssText 14604 }; 14605 }); 14606 14607 // Included from: js/tinymce/classes/Formatter.js 14608 14609 /** 14610 * Formatter.js 14611 * 14612 * Copyright, Moxiecode Systems AB 14613 * Released under LGPL License. 14614 * 14615 * License: http://www.tinymce.com/license 14616 * Contributing: http://www.tinymce.com/contributing 14617 */ 14618 14619 /** 14620 * Text formatter engine class. This class is used to apply formats like bold, italic, font size 14621 * etc to the current selection or specific nodes. This engine was build to replace the browsers 14622 * default formatting logic for execCommand due to it's inconsistent and buggy behavior. 14623 * 14624 * @class tinymce.Formatter 14625 * @example 14626 * tinymce.activeEditor.formatter.register('mycustomformat', { 14627 * inline: 'span', 14628 * styles: {color: '#ff0000'} 14629 * }); 14630 * 14631 * tinymce.activeEditor.formatter.apply('mycustomformat'); 14632 */ 14633 define("tinymce/Formatter", [ 14634 "tinymce/dom/TreeWalker", 14635 "tinymce/dom/RangeUtils", 14636 "tinymce/util/Tools", 14637 "tinymce/fmt/Preview" 14638 ], function(TreeWalker, RangeUtils, Tools, Preview) { 14639 /** 14640 * Constructs a new formatter instance. 14641 * 14642 * @constructor Formatter 14643 * @param {tinymce.Editor} ed Editor instance to construct the formatter engine to. 14644 */ 14645 return function(ed) { 14646 var formats = {}, 14647 dom = ed.dom, 14648 selection = ed.selection, 14649 rangeUtils = new RangeUtils(dom), 14650 isValid = ed.schema.isValidChild, 14651 isBlock = dom.isBlock, 14652 forcedRootBlock = ed.settings.forced_root_block, 14653 nodeIndex = dom.nodeIndex, 14654 INVISIBLE_CHAR = '\uFEFF', 14655 MCE_ATTR_RE = /^(src|href|style)$/, 14656 FALSE = false, 14657 TRUE = true, 14658 formatChangeData, 14659 undef, 14660 getContentEditable = dom.getContentEditable, 14661 disableCaretContainer, 14662 markCaretContainersBogus; 14663 14664 var each = Tools.each, 14665 grep = Tools.grep, 14666 walk = Tools.walk, 14667 extend = Tools.extend; 14668 14669 function isTextBlock(name) { 14670 if (name.nodeType) { 14671 name = name.nodeName; 14672 } 14673 14674 return !!ed.schema.getTextBlockElements()[name.toLowerCase()]; 14675 } 14676 14677 function getParents(node, selector) { 14678 return dom.getParents(node, selector, dom.getRoot()); 14679 } 14680 14681 function isCaretNode(node) { 14682 return node.nodeType === 1 && node.id === '_mce_caret'; 14683 } 14684 14685 function defaultFormats() { 14686 register({ 14687 14688 valigntop: [ 14689 {selector: 'td,th', styles: {'verticalAlign': 'top'}} 14690 ], 14691 14692 valignmiddle: [ 14693 {selector: 'td,th', styles: {'verticalAlign': 'middle'}} 14694 ], 14695 14696 valignbottom: [ 14697 {selector: 'td,th', styles: {'verticalAlign': 'bottom'}} 14698 ], 14699 14700 alignleft: [ 14701 {selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li', styles: {textAlign: 'left'}, defaultBlock: 'div'}, 14702 {selector: 'img,table', collapsed: false, styles: {'float': 'left'}} 14703 ], 14704 14705 aligncenter: [ 14706 {selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li', styles: {textAlign: 'center'}, defaultBlock: 'div'}, 14707 {selector: 'img', collapsed: false, styles: {display: 'block', marginLeft: 'auto', marginRight: 'auto'}}, 14708 {selector: 'table', collapsed: false, styles: {marginLeft: 'auto', marginRight: 'auto'}} 14709 ], 14710 14711 alignright: [ 14712 {selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li', styles: {textAlign: 'right'}, defaultBlock: 'div'}, 14713 {selector: 'img,table', collapsed: false, styles: {'float': 'right'}} 14714 ], 14715 14716 alignjustify: [ 14717 {selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li', styles: {textAlign: 'justify'}, defaultBlock: 'div'} 14718 ], 14719 14720 bold: [ 14721 {inline: 'strong', remove: 'all'}, 14722 {inline: 'span', styles: {fontWeight: 'bold'}}, 14723 {inline: 'b', remove: 'all'} 14724 ], 14725 14726 italic: [ 14727 {inline: 'em', remove: 'all'}, 14728 {inline: 'span', styles: {fontStyle: 'italic'}}, 14729 {inline: 'i', remove: 'all'} 14730 ], 14731 14732 underline: [ 14733 {inline: 'span', styles: {textDecoration: 'underline'}, exact: true}, 14734 {inline: 'u', remove: 'all'} 14735 ], 14736 14737 strikethrough: [ 14738 {inline: 'span', styles: {textDecoration: 'line-through'}, exact: true}, 14739 {inline: 'strike', remove: 'all'} 14740 ], 14741 14742 forecolor: {inline: 'span', styles: {color: '%value'}, wrap_links: false}, 14743 hilitecolor: {inline: 'span', styles: {backgroundColor: '%value'}, wrap_links: false}, 14744 fontname: {inline: 'span', styles: {fontFamily: '%value'}}, 14745 fontsize: {inline: 'span', styles: {fontSize: '%value'}}, 14746 fontsize_class: {inline: 'span', attributes: {'class': '%value'}}, 14747 blockquote: {block: 'blockquote', wrapper: 1, remove: 'all'}, 14748 subscript: {inline: 'sub'}, 14749 superscript: {inline: 'sup'}, 14750 code: {inline: 'code'}, 14751 14752 link: {inline: 'a', selector: 'a', remove: 'all', split: true, deep: true, 14753 onmatch: function() { 14754 return true; 14755 }, 14756 14757 onformat: function(elm, fmt, vars) { 14758 each(vars, function(value, key) { 14759 dom.setAttrib(elm, key, value); 14760 }); 14761 } 14762 }, 14763 14764 removeformat: [ 14765 { 14766 selector: 'b,strong,em,i,font,u,strike,sub,sup,dfn,code,samp,kbd,var,cite,mark,q', 14767 remove: 'all', 14768 split: true, 14769 expand: false, 14770 block_expand: true, 14771 deep: true 14772 }, 14773 {selector: 'span', attributes: ['style', 'class'], remove: 'empty', split: true, expand: false, deep: true}, 14774 {selector: '*', attributes: ['style', 'class'], split: false, expand: false, deep: true} 14775 ] 14776 }); 14777 14778 // Register default block formats 14779 each('p h1 h2 h3 h4 h5 h6 div address pre div dt dd samp'.split(/\s/), function(name) { 14780 register(name, {block: name, remove: 'all'}); 14781 }); 14782 14783 // Register user defined formats 14784 register(ed.settings.formats); 14785 } 14786 14787 function addKeyboardShortcuts() { 14788 // Add some inline shortcuts 14789 ed.addShortcut('ctrl+b', 'bold_desc', 'Bold'); 14790 ed.addShortcut('ctrl+i', 'italic_desc', 'Italic'); 14791 ed.addShortcut('ctrl+u', 'underline_desc', 'Underline'); 14792 14793 // BlockFormat shortcuts keys 14794 for (var i = 1; i <= 6; i++) { 14795 ed.addShortcut('ctrl+' + i, '', ['FormatBlock', false, 'h' + i]); 14796 } 14797 14798 ed.addShortcut('ctrl+7', '', ['FormatBlock', false, 'p']); 14799 ed.addShortcut('ctrl+8', '', ['FormatBlock', false, 'div']); 14800 ed.addShortcut('ctrl+9', '', ['FormatBlock', false, 'address']); 14801 } 14802 14803 // Public functions 14804 14805 /** 14806 * Returns the format by name or all formats if no name is specified. 14807 * 14808 * @method get 14809 * @param {String} name Optional name to retrive by. 14810 * @return {Array/Object} Array/Object with all registred formats or a specific format. 14811 */ 14812 function get(name) { 14813 return name ? formats[name] : formats; 14814 } 14815 14816 /** 14817 * Registers a specific format by name. 14818 * 14819 * @method register 14820 * @param {Object/String} name Name of the format for example "bold". 14821 * @param {Object/Array} format Optional format object or array of format variants 14822 * can only be omitted if the first arg is an object. 14823 */ 14824 function register(name, format) { 14825 if (name) { 14826 if (typeof(name) !== 'string') { 14827 each(name, function(format, name) { 14828 register(name, format); 14829 }); 14830 } else { 14831 // Force format into array and add it to internal collection 14832 format = format.length ? format : [format]; 14833 14834 each(format, function(format) { 14835 // Set deep to false by default on selector formats this to avoid removing 14836 // alignment on images inside paragraphs when alignment is changed on paragraphs 14837 if (format.deep === undef) { 14838 format.deep = !format.selector; 14839 } 14840 14841 // Default to true 14842 if (format.split === undef) { 14843 format.split = !format.selector || format.inline; 14844 } 14845 14846 // Default to true 14847 if (format.remove === undef && format.selector && !format.inline) { 14848 format.remove = 'none'; 14849 } 14850 14851 // Mark format as a mixed format inline + block level 14852 if (format.selector && format.inline) { 14853 format.mixed = true; 14854 format.block_expand = true; 14855 } 14856 14857 // Split classes if needed 14858 if (typeof(format.classes) === 'string') { 14859 format.classes = format.classes.split(/\s+/); 14860 } 14861 }); 14862 14863 formats[name] = format; 14864 } 14865 } 14866 } 14867 14868 function getTextDecoration(node) { 14869 var decoration; 14870 14871 ed.dom.getParent(node, function(n) { 14872 decoration = ed.dom.getStyle(n, 'text-decoration'); 14873 return decoration && decoration !== 'none'; 14874 }); 14875 14876 return decoration; 14877 } 14878 14879 function processUnderlineAndColor(node) { 14880 var textDecoration; 14881 if (node.nodeType === 1 && node.parentNode && node.parentNode.nodeType === 1) { 14882 textDecoration = getTextDecoration(node.parentNode); 14883 if (ed.dom.getStyle(node, 'color') && textDecoration) { 14884 ed.dom.setStyle(node, 'text-decoration', textDecoration); 14885 } else if (ed.dom.getStyle(node, 'textdecoration') === textDecoration) { 14886 ed.dom.setStyle(node, 'text-decoration', null); 14887 } 14888 } 14889 } 14890 14891 /** 14892 * Applies the specified format to the current selection or specified node. 14893 * 14894 * @method apply 14895 * @param {String} name Name of format to apply. 14896 * @param {Object} vars Optional list of variables to replace within format before applying it. 14897 * @param {Node} node Optional node to apply the format to defaults to current selection. 14898 */ 14899 function apply(name, vars, node) { 14900 var formatList = get(name), format = formatList[0], bookmark, rng, isCollapsed = !node && selection.isCollapsed(); 14901 14902 function setElementFormat(elm, fmt) { 14903 fmt = fmt || format; 14904 14905 if (elm) { 14906 if (fmt.onformat) { 14907 fmt.onformat(elm, fmt, vars, node); 14908 } 14909 14910 each(fmt.styles, function(value, name) { 14911 dom.setStyle(elm, name, replaceVars(value, vars)); 14912 }); 14913 14914 // Needed for the WebKit span spam bug 14915 // TODO: Remove this once WebKit/Blink fixes this 14916 if (fmt.styles) { 14917 var styleVal = dom.getAttrib(elm, 'style'); 14918 14919 if (styleVal) { 14920 elm.setAttribute('data-mce-style', styleVal); 14921 } 14922 } 14923 14924 each(fmt.attributes, function(value, name) { 14925 dom.setAttrib(elm, name, replaceVars(value, vars)); 14926 }); 14927 14928 each(fmt.classes, function(value) { 14929 value = replaceVars(value, vars); 14930 14931 if (!dom.hasClass(elm, value)) { 14932 dom.addClass(elm, value); 14933 } 14934 }); 14935 } 14936 } 14937 14938 function adjustSelectionToVisibleSelection() { 14939 function findSelectionEnd(start, end) { 14940 var walker = new TreeWalker(end); 14941 for (node = walker.current(); node; node = walker.prev()) { 14942 if (node.childNodes.length > 1 || node == start || node.tagName == 'BR') { 14943 return node; 14944 } 14945 } 14946 } 14947 14948 // Adjust selection so that a end container with a end offset of zero is not included in the selection 14949 // as this isn't visible to the user. 14950 var rng = ed.selection.getRng(); 14951 var start = rng.startContainer; 14952 var end = rng.endContainer; 14953 14954 if (start != end && rng.endOffset === 0) { 14955 var newEnd = findSelectionEnd(start, end); 14956 var endOffset = newEnd.nodeType == 3 ? newEnd.length : newEnd.childNodes.length; 14957 14958 rng.setEnd(newEnd, endOffset); 14959 } 14960 14961 return rng; 14962 } 14963 14964 function applyStyleToList(node, bookmark, wrapElm, newWrappers, process){ 14965 var nodes = [], listIndex = -1, list, startIndex = -1, endIndex = -1, currentWrapElm; 14966 14967 // find the index of the first child list. 14968 each(node.childNodes, function(n, index) { 14969 if (n.nodeName === "UL" || n.nodeName === "OL") { 14970 listIndex = index; 14971 list = n; 14972 return false; 14973 } 14974 }); 14975 14976 // get the index of the bookmarks 14977 each(node.childNodes, function(n, index) { 14978 if (n.nodeName === "SPAN" && dom.getAttrib(n, "data-mce-type") == "bookmark") { 14979 if (n.id == bookmark.id + "_start") { 14980 startIndex = index; 14981 } else if (n.id == bookmark.id + "_end") { 14982 endIndex = index; 14983 } 14984 } 14985 }); 14986 14987 // if the selection spans across an embedded list, or there isn't an embedded list - handle processing normally 14988 if (listIndex <= 0 || (startIndex < listIndex && endIndex > listIndex)) { 14989 each(grep(node.childNodes), process); 14990 return 0; 14991 } else { 14992 currentWrapElm = dom.clone(wrapElm, FALSE); 14993 14994 // create a list of the nodes on the same side of the list as the selection 14995 each(grep(node.childNodes), function(n, index) { 14996 if ((startIndex < listIndex && index < listIndex) || (startIndex > listIndex && index > listIndex)) { 14997 nodes.push(n); 14998 n.parentNode.removeChild(n); 14999 } 15000 }); 15001 15002 // insert the wrapping element either before or after the list. 15003 if (startIndex < listIndex) { 15004 node.insertBefore(currentWrapElm, list); 15005 } else if (startIndex > listIndex) { 15006 node.insertBefore(currentWrapElm, list.nextSibling); 15007 } 15008 15009 // add the new nodes to the list. 15010 newWrappers.push(currentWrapElm); 15011 15012 each(nodes, function(node) { 15013 currentWrapElm.appendChild(node); 15014 }); 15015 15016 return currentWrapElm; 15017 } 15018 } 15019 15020 function applyRngStyle(rng, bookmark, node_specific) { 15021 var newWrappers = [], wrapName, wrapElm, contentEditable = true; 15022 15023 // Setup wrapper element 15024 wrapName = format.inline || format.block; 15025 wrapElm = dom.create(wrapName); 15026 setElementFormat(wrapElm); 15027 15028 rangeUtils.walk(rng, function(nodes) { 15029 var currentWrapElm; 15030 15031 /** 15032 * Process a list of nodes wrap them. 15033 */ 15034 function process(node) { 15035 var nodeName, parentName, found, hasContentEditableState, lastContentEditable; 15036 15037 lastContentEditable = contentEditable; 15038 nodeName = node.nodeName.toLowerCase(); 15039 parentName = node.parentNode.nodeName.toLowerCase(); 15040 15041 // Node has a contentEditable value 15042 if (node.nodeType === 1 && getContentEditable(node)) { 15043 lastContentEditable = contentEditable; 15044 contentEditable = getContentEditable(node) === "true"; 15045 hasContentEditableState = true; // We don't want to wrap the container only it's children 15046 } 15047 15048 // Stop wrapping on br elements 15049 if (isEq(nodeName, 'br')) { 15050 currentWrapElm = 0; 15051 15052 // Remove any br elements when we wrap things 15053 if (format.block) { 15054 dom.remove(node); 15055 } 15056 15057 return; 15058 } 15059 15060 // If node is wrapper type 15061 if (format.wrapper && matchNode(node, name, vars)) { 15062 currentWrapElm = 0; 15063 return; 15064 } 15065 15066 // Can we rename the block 15067 // TODO: Break this if up, too complex 15068 if (contentEditable && !hasContentEditableState && format.block && 15069 !format.wrapper && isTextBlock(nodeName) && isValid(parentName, wrapName)) { 15070 node = dom.rename(node, wrapName); 15071 setElementFormat(node); 15072 newWrappers.push(node); 15073 currentWrapElm = 0; 15074 return; 15075 } 15076 15077 // Handle selector patterns 15078 if (format.selector) { 15079 // Look for matching formats 15080 each(formatList, function(format) { 15081 // Check collapsed state if it exists 15082 if ('collapsed' in format && format.collapsed !== isCollapsed) { 15083 return; 15084 } 15085 15086 if (dom.is(node, format.selector) && !isCaretNode(node)) { 15087 setElementFormat(node, format); 15088 found = true; 15089 } 15090 }); 15091 15092 // Continue processing if a selector match wasn't found and a inline element is defined 15093 if (!format.inline || found) { 15094 currentWrapElm = 0; 15095 return; 15096 } 15097 } 15098 15099 // Is it valid to wrap this item 15100 // TODO: Break this if up, too complex 15101 if (contentEditable && !hasContentEditableState && isValid(wrapName, nodeName) && isValid(parentName, wrapName) && 15102 !(!node_specific && node.nodeType === 3 && 15103 node.nodeValue.length === 1 && 15104 node.nodeValue.charCodeAt(0) === 65279) && 15105 !isCaretNode(node) && 15106 (!format.inline || !isBlock(node))) { 15107 // Start wrapping 15108 if (!currentWrapElm) { 15109 // Wrap the node 15110 currentWrapElm = dom.clone(wrapElm, FALSE); 15111 node.parentNode.insertBefore(currentWrapElm, node); 15112 newWrappers.push(currentWrapElm); 15113 } 15114 15115 currentWrapElm.appendChild(node); 15116 } else if (nodeName == 'li' && bookmark) { 15117 // Start wrapping - if we are in a list node and have a bookmark, then 15118 // we will always begin by wrapping in a new element. 15119 currentWrapElm = applyStyleToList(node, bookmark, wrapElm, newWrappers, process); 15120 } else { 15121 // Start a new wrapper for possible children 15122 currentWrapElm = 0; 15123 15124 each(grep(node.childNodes), process); 15125 15126 if (hasContentEditableState) { 15127 contentEditable = lastContentEditable; // Restore last contentEditable state from stack 15128 } 15129 15130 // End the last wrapper 15131 currentWrapElm = 0; 15132 } 15133 } 15134 15135 // Process siblings from range 15136 each(nodes, process); 15137 }); 15138 15139 // Wrap links inside as well, for example color inside a link when the wrapper is around the link 15140 if (format.wrap_links === false) { 15141 each(newWrappers, function(node) { 15142 function process(node) { 15143 var i, currentWrapElm, children; 15144 15145 if (node.nodeName === 'A') { 15146 currentWrapElm = dom.clone(wrapElm, FALSE); 15147 newWrappers.push(currentWrapElm); 15148 15149 children = grep(node.childNodes); 15150 for (i = 0; i < children.length; i++) { 15151 currentWrapElm.appendChild(children[i]); 15152 } 15153 15154 node.appendChild(currentWrapElm); 15155 } 15156 15157 each(grep(node.childNodes), process); 15158 } 15159 15160 process(node); 15161 }); 15162 } 15163 15164 // Cleanup 15165 each(newWrappers, function(node) { 15166 var childCount; 15167 15168 function getChildCount(node) { 15169 var count = 0; 15170 15171 each(node.childNodes, function(node) { 15172 if (!isWhiteSpaceNode(node) && !isBookmarkNode(node)) { 15173 count++; 15174 } 15175 }); 15176 15177 return count; 15178 } 15179 15180 function mergeStyles(node) { 15181 var child, clone; 15182 15183 each(node.childNodes, function(node) { 15184 if (node.nodeType == 1 && !isBookmarkNode(node) && !isCaretNode(node)) { 15185 child = node; 15186 return FALSE; // break loop 15187 } 15188 }); 15189 15190 // If child was found and of the same type as the current node 15191 if (child && !isBookmarkNode(child) && matchName(child, format)) { 15192 clone = dom.clone(child, FALSE); 15193 setElementFormat(clone); 15194 15195 dom.replace(clone, node, TRUE); 15196 dom.remove(child, 1); 15197 } 15198 15199 return clone || node; 15200 } 15201 15202 childCount = getChildCount(node); 15203 15204 // Remove empty nodes but only if there is multiple wrappers and they are not block 15205 // elements so never remove single <h1></h1> since that would remove the 15206 // currrent empty block element where the caret is at 15207 if ((newWrappers.length > 1 || !isBlock(node)) && childCount === 0) { 15208 dom.remove(node, 1); 15209 return; 15210 } 15211 15212 if (format.inline || format.wrapper) { 15213 // Merges the current node with it's children of similar type to reduce the number of elements 15214 if (!format.exact && childCount === 1) { 15215 node = mergeStyles(node); 15216 } 15217 15218 // Remove/merge children 15219 each(formatList, function(format) { 15220 // Merge all children of similar type will move styles from child to parent 15221 // this: <span style="color:red"><b><span style="color:red; font-size:10px">text</span></b></span> 15222 // will become: <span style="color:red"><b><span style="font-size:10px">text</span></b></span> 15223 each(dom.select(format.inline, node), function(child) { 15224 var parent; 15225 15226 if (isBookmarkNode(child)) { 15227 return; 15228 } 15229 15230 // When wrap_links is set to false we don't want 15231 // to remove the format on children within links 15232 if (format.wrap_links === false) { 15233 parent = child.parentNode; 15234 15235 do { 15236 if (parent.nodeName === 'A') { 15237 return; 15238 } 15239 } while ((parent = parent.parentNode)); 15240 } 15241 15242 removeFormat(format, vars, child, format.exact ? child : null); 15243 }); 15244 }); 15245 15246 // Remove child if direct parent is of same type 15247 if (matchNode(node.parentNode, name, vars)) { 15248 dom.remove(node, 1); 15249 node = 0; 15250 return TRUE; 15251 } 15252 15253 // Look for parent with similar style format 15254 if (format.merge_with_parents) { 15255 dom.getParent(node.parentNode, function(parent) { 15256 if (matchNode(parent, name, vars)) { 15257 dom.remove(node, 1); 15258 node = 0; 15259 return TRUE; 15260 } 15261 }); 15262 } 15263 15264 // Merge next and previous siblings if they are similar <b>text</b><b>text</b> becomes <b>texttext</b> 15265 if (node && format.merge_siblings !== false) { 15266 node = mergeSiblings(getNonWhiteSpaceSibling(node), node); 15267 node = mergeSiblings(node, getNonWhiteSpaceSibling(node, TRUE)); 15268 } 15269 } 15270 }); 15271 } 15272 15273 if (format) { 15274 if (node) { 15275 if (node.nodeType) { 15276 rng = dom.createRng(); 15277 rng.setStartBefore(node); 15278 rng.setEndAfter(node); 15279 applyRngStyle(expandRng(rng, formatList), null, true); 15280 } else { 15281 applyRngStyle(node, null, true); 15282 } 15283 } else { 15284 if (!isCollapsed || !format.inline || dom.select('td.mce-item-selected,th.mce-item-selected').length) { 15285 // Obtain selection node before selection is unselected by applyRngStyle() 15286 var curSelNode = ed.selection.getNode(); 15287 15288 // If the formats have a default block and we can't find a parent block then 15289 // start wrapping it with a DIV this is for forced_root_blocks: false 15290 // It's kind of a hack but people should be using the default block type P since all desktop editors work that way 15291 if (!forcedRootBlock && formatList[0].defaultBlock && !dom.getParent(curSelNode, dom.isBlock)) { 15292 apply(formatList[0].defaultBlock); 15293 } 15294 15295 // Apply formatting to selection 15296 ed.selection.setRng(adjustSelectionToVisibleSelection()); 15297 bookmark = selection.getBookmark(); 15298 applyRngStyle(expandRng(selection.getRng(TRUE), formatList), bookmark); 15299 15300 // Colored nodes should be underlined so that the color of the underline matches the text color. 15301 if (format.styles && (format.styles.color || format.styles.textDecoration)) { 15302 walk(curSelNode, processUnderlineAndColor, 'childNodes'); 15303 processUnderlineAndColor(curSelNode); 15304 } 15305 15306 selection.moveToBookmark(bookmark); 15307 moveStart(selection.getRng(TRUE)); 15308 ed.nodeChanged(); 15309 } else { 15310 performCaretAction('apply', name, vars); 15311 } 15312 } 15313 } 15314 } 15315 15316 /** 15317 * Removes the specified format from the current selection or specified node. 15318 * 15319 * @method remove 15320 * @param {String} name Name of format to remove. 15321 * @param {Object} vars Optional list of variables to replace within format before removing it. 15322 * @param {Node/Range} node Optional node or DOM range to remove the format from defaults to current selection. 15323 */ 15324 function remove(name, vars, node) { 15325 var formatList = get(name), format = formatList[0], bookmark, rng, contentEditable = true; 15326 15327 // Merges the styles for each node 15328 function process(node) { 15329 var children, i, l, lastContentEditable, hasContentEditableState; 15330 15331 // Node has a contentEditable value 15332 if (node.nodeType === 1 && getContentEditable(node)) { 15333 lastContentEditable = contentEditable; 15334 contentEditable = getContentEditable(node) === "true"; 15335 hasContentEditableState = true; // We don't want to wrap the container only it's children 15336 } 15337 15338 // Grab the children first since the nodelist might be changed 15339 children = grep(node.childNodes); 15340 15341 // Process current node 15342 if (contentEditable && !hasContentEditableState) { 15343 for (i = 0, l = formatList.length; i < l; i++) { 15344 if (removeFormat(formatList[i], vars, node, node)) { 15345 break; 15346 } 15347 } 15348 } 15349 15350 // Process the children 15351 if (format.deep) { 15352 if (children.length) { 15353 for (i = 0, l = children.length; i < l; i++) { 15354 process(children[i]); 15355 } 15356 15357 if (hasContentEditableState) { 15358 contentEditable = lastContentEditable; // Restore last contentEditable state from stack 15359 } 15360 } 15361 } 15362 } 15363 15364 function findFormatRoot(container) { 15365 var formatRoot; 15366 15367 // Find format root 15368 each(getParents(container.parentNode).reverse(), function(parent) { 15369 var format; 15370 15371 // Find format root element 15372 if (!formatRoot && parent.id != '_start' && parent.id != '_end') { 15373 // Is the node matching the format we are looking for 15374 format = matchNode(parent, name, vars); 15375 if (format && format.split !== false) { 15376 formatRoot = parent; 15377 } 15378 } 15379 }); 15380 15381 return formatRoot; 15382 } 15383 15384 function wrapAndSplit(format_root, container, target, split) { 15385 var parent, clone, lastClone, firstClone, i, formatRootParent; 15386 15387 // Format root found then clone formats and split it 15388 if (format_root) { 15389 formatRootParent = format_root.parentNode; 15390 15391 for (parent = container.parentNode; parent && parent != formatRootParent; parent = parent.parentNode) { 15392 clone = dom.clone(parent, FALSE); 15393 15394 for (i = 0; i < formatList.length; i++) { 15395 if (removeFormat(formatList[i], vars, clone, clone)) { 15396 clone = 0; 15397 break; 15398 } 15399 } 15400 15401 // Build wrapper node 15402 if (clone) { 15403 if (lastClone) { 15404 clone.appendChild(lastClone); 15405 } 15406 15407 if (!firstClone) { 15408 firstClone = clone; 15409 } 15410 15411 lastClone = clone; 15412 } 15413 } 15414 15415 // Never split block elements if the format is mixed 15416 if (split && (!format.mixed || !isBlock(format_root))) { 15417 container = dom.split(format_root, container); 15418 } 15419 15420 // Wrap container in cloned formats 15421 if (lastClone) { 15422 target.parentNode.insertBefore(lastClone, target); 15423 firstClone.appendChild(target); 15424 } 15425 } 15426 15427 return container; 15428 } 15429 15430 function splitToFormatRoot(container) { 15431 return wrapAndSplit(findFormatRoot(container), container, container, true); 15432 } 15433 15434 function unwrap(start) { 15435 var node = dom.get(start ? '_start' : '_end'), 15436 out = node[start ? 'firstChild' : 'lastChild']; 15437 15438 // If the end is placed within the start the result will be removed 15439 // So this checks if the out node is a bookmark node if it is it 15440 // checks for another more suitable node 15441 if (isBookmarkNode(out)) { 15442 out = out[start ? 'firstChild' : 'lastChild']; 15443 } 15444 15445 dom.remove(node, true); 15446 15447 return out; 15448 } 15449 15450 function removeRngStyle(rng) { 15451 var startContainer, endContainer; 15452 var commonAncestorContainer = rng.commonAncestorContainer; 15453 15454 rng = expandRng(rng, formatList, TRUE); 15455 15456 if (format.split) { 15457 startContainer = getContainer(rng, TRUE); 15458 endContainer = getContainer(rng); 15459 15460 if (startContainer != endContainer) { 15461 // WebKit will render the table incorrectly if we wrap a TH or TD in a SPAN 15462 // so let's see if we can use the first child instead 15463 // This will happen if you triple click a table cell and use remove formatting 15464 if (/^(TR|TH|TD)$/.test(startContainer.nodeName) && startContainer.firstChild) { 15465 if (startContainer.nodeName == "TR") { 15466 startContainer = startContainer.firstChild.firstChild || startContainer; 15467 } else { 15468 startContainer = startContainer.firstChild || startContainer; 15469 } 15470 } 15471 15472 // Try to adjust endContainer as well if cells on the same row were selected - bug #6410 15473 if (commonAncestorContainer && 15474 /^T(HEAD|BODY|FOOT|R)$/.test(commonAncestorContainer.nodeName) && 15475 /^(TH|TD)$/.test(endContainer.nodeName) && endContainer.firstChild) { 15476 endContainer = endContainer.firstChild || endContainer; 15477 } 15478 15479 // Wrap start/end nodes in span element since these might be cloned/moved 15480 startContainer = wrap(startContainer, 'span', {id: '_start', 'data-mce-type': 'bookmark'}); 15481 endContainer = wrap(endContainer, 'span', {id: '_end', 'data-mce-type': 'bookmark'}); 15482 15483 // Split start/end 15484 splitToFormatRoot(startContainer); 15485 splitToFormatRoot(endContainer); 15486 15487 // Unwrap start/end to get real elements again 15488 startContainer = unwrap(TRUE); 15489 endContainer = unwrap(); 15490 } else { 15491 startContainer = endContainer = splitToFormatRoot(startContainer); 15492 } 15493 15494 // Update range positions since they might have changed after the split operations 15495 rng.startContainer = startContainer.parentNode; 15496 rng.startOffset = nodeIndex(startContainer); 15497 rng.endContainer = endContainer.parentNode; 15498 rng.endOffset = nodeIndex(endContainer) + 1; 15499 } 15500 15501 // Remove items between start/end 15502 rangeUtils.walk(rng, function(nodes) { 15503 each(nodes, function(node) { 15504 process(node); 15505 15506 // Remove parent span if it only contains text-decoration: underline, yet a parent node is also underlined. 15507 if (node.nodeType === 1 && ed.dom.getStyle(node, 'text-decoration') === 'underline' && 15508 node.parentNode && getTextDecoration(node.parentNode) === 'underline') { 15509 removeFormat({ 15510 'deep': false, 15511 'exact': true, 15512 'inline': 'span', 15513 'styles': { 15514 'textDecoration': 'underline' 15515 } 15516 }, null, node); 15517 } 15518 }); 15519 }); 15520 } 15521 15522 // Handle node 15523 if (node) { 15524 if (node.nodeType) { 15525 rng = dom.createRng(); 15526 rng.setStartBefore(node); 15527 rng.setEndAfter(node); 15528 removeRngStyle(rng); 15529 } else { 15530 removeRngStyle(node); 15531 } 15532 15533 return; 15534 } 15535 15536 if (!selection.isCollapsed() || !format.inline || dom.select('td.mce-item-selected,th.mce-item-selected').length) { 15537 bookmark = selection.getBookmark(); 15538 removeRngStyle(selection.getRng(TRUE)); 15539 selection.moveToBookmark(bookmark); 15540 15541 // Check if start element still has formatting then we are at: "<b>text|</b>text" 15542 // and need to move the start into the next text node 15543 if (format.inline && match(name, vars, selection.getStart())) { 15544 moveStart(selection.getRng(true)); 15545 } 15546 15547 ed.nodeChanged(); 15548 } else { 15549 performCaretAction('remove', name, vars); 15550 } 15551 } 15552 15553 /** 15554 * Toggles the specified format on/off. 15555 * 15556 * @method toggle 15557 * @param {String} name Name of format to apply/remove. 15558 * @param {Object} vars Optional list of variables to replace within format before applying/removing it. 15559 * @param {Node} node Optional node to apply the format to or remove from. Defaults to current selection. 15560 */ 15561 function toggle(name, vars, node) { 15562 var fmt = get(name); 15563 15564 if (match(name, vars, node) && (!('toggle' in fmt[0]) || fmt[0].toggle)) { 15565 remove(name, vars, node); 15566 } else { 15567 apply(name, vars, node); 15568 } 15569 } 15570 15571 /** 15572 * Return true/false if the specified node has the specified format. 15573 * 15574 * @method matchNode 15575 * @param {Node} node Node to check the format on. 15576 * @param {String} name Format name to check. 15577 * @param {Object} vars Optional list of variables to replace before checking it. 15578 * @param {Boolean} similar Match format that has similar properties. 15579 * @return {Object} Returns the format object it matches or undefined if it doesn't match. 15580 */ 15581 function matchNode(node, name, vars, similar) { 15582 var formatList = get(name), format, i, classes; 15583 15584 function matchItems(node, format, item_name) { 15585 var key, value, items = format[item_name], i; 15586 15587 // Custom match 15588 if (format.onmatch) { 15589 return format.onmatch(node, format, item_name); 15590 } 15591 15592 // Check all items 15593 if (items) { 15594 // Non indexed object 15595 if (items.length === undef) { 15596 for (key in items) { 15597 if (items.hasOwnProperty(key)) { 15598 if (item_name === 'attributes') { 15599 value = dom.getAttrib(node, key); 15600 } else { 15601 value = getStyle(node, key); 15602 } 15603 15604 if (similar && !value && !format.exact) { 15605 return; 15606 } 15607 15608 if ((!similar || format.exact) && !isEq(value, normalizeStyleValue(replaceVars(items[key], vars), key))) { 15609 return; 15610 } 15611 } 15612 } 15613 } else { 15614 // Only one match needed for indexed arrays 15615 for (i = 0; i < items.length; i++) { 15616 if (item_name === 'attributes' ? dom.getAttrib(node, items[i]) : getStyle(node, items[i])) { 15617 return format; 15618 } 15619 } 15620 } 15621 } 15622 15623 return format; 15624 } 15625 15626 if (formatList && node) { 15627 // Check each format in list 15628 for (i = 0; i < formatList.length; i++) { 15629 format = formatList[i]; 15630 15631 // Name name, attributes, styles and classes 15632 if (matchName(node, format) && matchItems(node, format, 'attributes') && matchItems(node, format, 'styles')) { 15633 // Match classes 15634 if ((classes = format.classes)) { 15635 for (i = 0; i < classes.length; i++) { 15636 if (!dom.hasClass(node, classes[i])) { 15637 return; 15638 } 15639 } 15640 } 15641 15642 return format; 15643 } 15644 } 15645 } 15646 } 15647 15648 /** 15649 * Matches the current selection or specified node against the specified format name. 15650 * 15651 * @method match 15652 * @param {String} name Name of format to match. 15653 * @param {Object} vars Optional list of variables to replace before checking it. 15654 * @param {Node} node Optional node to check. 15655 * @return {boolean} true/false if the specified selection/node matches the format. 15656 */ 15657 function match(name, vars, node) { 15658 var startNode; 15659 15660 function matchParents(node) { 15661 var root = dom.getRoot(); 15662 15663 if (node === root) { 15664 return false; 15665 } 15666 15667 // Find first node with similar format settings 15668 node = dom.getParent(node, function(node) { 15669 return node.parentNode === root || !!matchNode(node, name, vars, true); 15670 }); 15671 15672 // Do an exact check on the similar format element 15673 return matchNode(node, name, vars); 15674 } 15675 15676 // Check specified node 15677 if (node) { 15678 return matchParents(node); 15679 } 15680 15681 // Check selected node 15682 node = selection.getNode(); 15683 if (matchParents(node)) { 15684 return TRUE; 15685 } 15686 15687 // Check start node if it's different 15688 startNode = selection.getStart(); 15689 if (startNode != node) { 15690 if (matchParents(startNode)) { 15691 return TRUE; 15692 } 15693 } 15694 15695 return FALSE; 15696 } 15697 15698 /** 15699 * Matches the current selection against the array of formats and returns a new array with matching formats. 15700 * 15701 * @method matchAll 15702 * @param {Array} names Name of format to match. 15703 * @param {Object} vars Optional list of variables to replace before checking it. 15704 * @return {Array} Array with matched formats. 15705 */ 15706 function matchAll(names, vars) { 15707 var startElement, matchedFormatNames = [], checkedMap = {}; 15708 15709 // Check start of selection for formats 15710 startElement = selection.getStart(); 15711 dom.getParent(startElement, function(node) { 15712 var i, name; 15713 15714 for (i = 0; i < names.length; i++) { 15715 name = names[i]; 15716 15717 if (!checkedMap[name] && matchNode(node, name, vars)) { 15718 checkedMap[name] = true; 15719 matchedFormatNames.push(name); 15720 } 15721 } 15722 }, dom.getRoot()); 15723 15724 return matchedFormatNames; 15725 } 15726 15727 /** 15728 * Returns true/false if the specified format can be applied to the current selection or not. It 15729 * will currently only check the state for selector formats, it returns true on all other format types. 15730 * 15731 * @method canApply 15732 * @param {String} name Name of format to check. 15733 * @return {boolean} true/false if the specified format can be applied to the current selection/node. 15734 */ 15735 function canApply(name) { 15736 var formatList = get(name), startNode, parents, i, x, selector; 15737 15738 if (formatList) { 15739 startNode = selection.getStart(); 15740 parents = getParents(startNode); 15741 15742 for (x = formatList.length - 1; x >= 0; x--) { 15743 selector = formatList[x].selector; 15744 15745 // Format is not selector based then always return TRUE 15746 // Is it has a defaultBlock then it's likely it can be applied for example align on a non block element line 15747 if (!selector || formatList[x].defaultBlock) { 15748 return TRUE; 15749 } 15750 15751 for (i = parents.length - 1; i >= 0; i--) { 15752 if (dom.is(parents[i], selector)) { 15753 return TRUE; 15754 } 15755 } 15756 } 15757 } 15758 15759 return FALSE; 15760 } 15761 15762 /** 15763 * Executes the specified callback when the current selection matches the formats or not. 15764 * 15765 * @method formatChanged 15766 * @param {String} formats Comma separated list of formats to check for. 15767 * @param {function} callback Callback with state and args when the format is changed/toggled on/off. 15768 * @param {Boolean} similar True/false state if the match should handle similar or exact formats. 15769 */ 15770 function formatChanged(formats, callback, similar) { 15771 var currentFormats; 15772 15773 // Setup format node change logic 15774 if (!formatChangeData) { 15775 formatChangeData = {}; 15776 currentFormats = {}; 15777 15778 ed.on('NodeChange', function(e) { 15779 var parents = getParents(e.element), matchedFormats = {}; 15780 15781 // Check for new formats 15782 each(formatChangeData, function(callbacks, format) { 15783 each(parents, function(node) { 15784 if (matchNode(node, format, {}, callbacks.similar)) { 15785 if (!currentFormats[format]) { 15786 // Execute callbacks 15787 each(callbacks, function(callback) { 15788 callback(true, {node: node, format: format, parents: parents}); 15789 }); 15790 15791 currentFormats[format] = callbacks; 15792 } 15793 15794 matchedFormats[format] = callbacks; 15795 return false; 15796 } 15797 }); 15798 }); 15799 15800 // Check if current formats still match 15801 each(currentFormats, function(callbacks, format) { 15802 if (!matchedFormats[format]) { 15803 delete currentFormats[format]; 15804 15805 each(callbacks, function(callback) { 15806 callback(false, {node: e.element, format: format, parents: parents}); 15807 }); 15808 } 15809 }); 15810 }); 15811 } 15812 15813 // Add format listeners 15814 each(formats.split(','), function(format) { 15815 if (!formatChangeData[format]) { 15816 formatChangeData[format] = []; 15817 formatChangeData[format].similar = similar; 15818 } 15819 15820 formatChangeData[format].push(callback); 15821 }); 15822 15823 return this; 15824 } 15825 15826 /** 15827 * Returns a preview css text for the specified format. 15828 * 15829 * @method getCssText 15830 * @param {String/Object} format Format to generate preview css text for. 15831 * @return {String} Css text for the specified format. 15832 * @example 15833 * var cssText1 = editor.formatter.getCssText('bold'); 15834 * var cssText2 = editor.formatter.getCssText({inline: 'b'}); 15835 */ 15836 function getCssText(format) { 15837 return Preview.getCssText(ed, format); 15838 } 15839 15840 // Expose to public 15841 extend(this, { 15842 get: get, 15843 register: register, 15844 apply: apply, 15845 remove: remove, 15846 toggle: toggle, 15847 match: match, 15848 matchAll: matchAll, 15849 matchNode: matchNode, 15850 canApply: canApply, 15851 formatChanged: formatChanged, 15852 getCssText: getCssText 15853 }); 15854 15855 // Initialize 15856 defaultFormats(); 15857 addKeyboardShortcuts(); 15858 ed.on('BeforeGetContent', function() { 15859 if (markCaretContainersBogus) { 15860 markCaretContainersBogus(); 15861 } 15862 }); 15863 ed.on('mouseup keydown', function(e) { 15864 if (disableCaretContainer) { 15865 disableCaretContainer(e); 15866 } 15867 }); 15868 15869 // Private functions 15870 15871 /** 15872 * Checks if the specified nodes name matches the format inline/block or selector. 15873 * 15874 * @private 15875 * @param {Node} node Node to match against the specified format. 15876 * @param {Object} format Format object o match with. 15877 * @return {boolean} true/false if the format matches. 15878 */ 15879 function matchName(node, format) { 15880 // Check for inline match 15881 if (isEq(node, format.inline)) { 15882 return TRUE; 15883 } 15884 15885 // Check for block match 15886 if (isEq(node, format.block)) { 15887 return TRUE; 15888 } 15889 15890 // Check for selector match 15891 if (format.selector) { 15892 return node.nodeType == 1 && dom.is(node, format.selector); 15893 } 15894 } 15895 15896 /** 15897 * Compares two string/nodes regardless of their case. 15898 * 15899 * @private 15900 * @param {String/Node} Node or string to compare. 15901 * @param {String/Node} Node or string to compare. 15902 * @return {boolean} True/false if they match. 15903 */ 15904 function isEq(str1, str2) { 15905 str1 = str1 || ''; 15906 str2 = str2 || ''; 15907 15908 str1 = '' + (str1.nodeName || str1); 15909 str2 = '' + (str2.nodeName || str2); 15910 15911 return str1.toLowerCase() == str2.toLowerCase(); 15912 } 15913 15914 /** 15915 * Returns the style by name on the specified node. This method modifies the style 15916 * contents to make it more easy to match. This will resolve a few browser issues. 15917 * 15918 * @private 15919 * @param {Node} node to get style from. 15920 * @param {String} name Style name to get. 15921 * @return {String} Style item value. 15922 */ 15923 function getStyle(node, name) { 15924 return normalizeStyleValue(dom.getStyle(node, name), name); 15925 } 15926 15927 /** 15928 * Normalize style value by name. This method modifies the style contents 15929 * to make it more easy to match. This will resolve a few browser issues. 15930 * 15931 * @private 15932 * @param {Node} node to get style from. 15933 * @param {String} name Style name to get. 15934 * @return {String} Style item value. 15935 */ 15936 function normalizeStyleValue(value, name) { 15937 // Force the format to hex 15938 if (name == 'color' || name == 'backgroundColor') { 15939 value = dom.toHex(value); 15940 } 15941 15942 // Opera will return bold as 700 15943 if (name == 'fontWeight' && value == 700) { 15944 value = 'bold'; 15945 } 15946 15947 // Normalize fontFamily so "'Font name', Font" becomes: "Font name,Font" 15948 if (name == 'fontFamily') { 15949 value = value.replace(/[\'\"]/g, '').replace(/,\s+/g, ','); 15950 } 15951 15952 return '' + value; 15953 } 15954 15955 /** 15956 * Replaces variables in the value. The variable format is %var. 15957 * 15958 * @private 15959 * @param {String} value Value to replace variables in. 15960 * @param {Object} vars Name/value array with variables to replace. 15961 * @return {String} New value with replaced variables. 15962 */ 15963 function replaceVars(value, vars) { 15964 if (typeof(value) != "string") { 15965 value = value(vars); 15966 } else if (vars) { 15967 value = value.replace(/%(\w+)/g, function(str, name) { 15968 return vars[name] || str; 15969 }); 15970 } 15971 15972 return value; 15973 } 15974 15975 function isWhiteSpaceNode(node) { 15976 return node && node.nodeType === 3 && /^([\t \r\n]+|)$/.test(node.nodeValue); 15977 } 15978 15979 function wrap(node, name, attrs) { 15980 var wrapper = dom.create(name, attrs); 15981 15982 node.parentNode.insertBefore(wrapper, node); 15983 wrapper.appendChild(node); 15984 15985 return wrapper; 15986 } 15987 15988 /** 15989 * Expands the specified range like object to depending on format. 15990 * 15991 * For example on block formats it will move the start/end position 15992 * to the beginning of the current block. 15993 * 15994 * @private 15995 * @param {Object} rng Range like object. 15996 * @param {Array} formats Array with formats to expand by. 15997 * @return {Object} Expanded range like object. 15998 */ 15999 function expandRng(rng, format, remove) { 16000 var lastIdx, leaf, endPoint, 16001 startContainer = rng.startContainer, 16002 startOffset = rng.startOffset, 16003 endContainer = rng.endContainer, 16004 endOffset = rng.endOffset; 16005 16006 // This function walks up the tree if there is no siblings before/after the node 16007 function findParentContainer(start) { 16008 var container, parent, sibling, siblingName, root; 16009 16010 container = parent = start ? startContainer : endContainer; 16011 siblingName = start ? 'previousSibling' : 'nextSibling'; 16012 root = dom.getRoot(); 16013 16014 function isBogusBr(node) { 16015 return node.nodeName == "BR" && node.getAttribute('data-mce-bogus') && !node.nextSibling; 16016 } 16017 16018 // If it's a text node and the offset is inside the text 16019 if (container.nodeType == 3 && !isWhiteSpaceNode(container)) { 16020 if (start ? startOffset > 0 : endOffset < container.nodeValue.length) { 16021 return container; 16022 } 16023 } 16024 16025 /*eslint no-constant-condition:0 */ 16026 while (true) { 16027 // Stop expanding on block elements 16028 if (!format[0].block_expand && isBlock(parent)) { 16029 return parent; 16030 } 16031 16032 // Walk left/right 16033 for (sibling = parent[siblingName]; sibling; sibling = sibling[siblingName]) { 16034 if (!isBookmarkNode(sibling) && !isWhiteSpaceNode(sibling) && !isBogusBr(sibling)) { 16035 return parent; 16036 } 16037 } 16038 16039 // Check if we can move up are we at root level or body level 16040 if (parent.parentNode == root) { 16041 container = parent; 16042 break; 16043 } 16044 16045 parent = parent.parentNode; 16046 } 16047 16048 return container; 16049 } 16050 16051 // This function walks down the tree to find the leaf at the selection. 16052 // The offset is also returned as if node initially a leaf, the offset may be in the middle of the text node. 16053 function findLeaf(node, offset) { 16054 if (offset === undef) { 16055 offset = node.nodeType === 3 ? node.length : node.childNodes.length; 16056 } 16057 16058 while (node && node.hasChildNodes()) { 16059 node = node.childNodes[offset]; 16060 if (node) { 16061 offset = node.nodeType === 3 ? node.length : node.childNodes.length; 16062 } 16063 } 16064 return { node: node, offset: offset }; 16065 } 16066 16067 // If index based start position then resolve it 16068 if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) { 16069 lastIdx = startContainer.childNodes.length - 1; 16070 startContainer = startContainer.childNodes[startOffset > lastIdx ? lastIdx : startOffset]; 16071 16072 if (startContainer.nodeType == 3) { 16073 startOffset = 0; 16074 } 16075 } 16076 16077 // If index based end position then resolve it 16078 if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) { 16079 lastIdx = endContainer.childNodes.length - 1; 16080 endContainer = endContainer.childNodes[endOffset > lastIdx ? lastIdx : endOffset - 1]; 16081 16082 if (endContainer.nodeType == 3) { 16083 endOffset = endContainer.nodeValue.length; 16084 } 16085 } 16086 16087 // Expands the node to the closes contentEditable false element if it exists 16088 function findParentContentEditable(node) { 16089 var parent = node; 16090 16091 while (parent) { 16092 if (parent.nodeType === 1 && getContentEditable(parent)) { 16093 return getContentEditable(parent) === "false" ? parent : node; 16094 } 16095 16096 parent = parent.parentNode; 16097 } 16098 16099 return node; 16100 } 16101 16102 function findWordEndPoint(container, offset, start) { 16103 var walker, node, pos, lastTextNode; 16104 16105 function findSpace(node, offset) { 16106 var pos, pos2, str = node.nodeValue; 16107 16108 if (typeof(offset) == "undefined") { 16109 offset = start ? str.length : 0; 16110 } 16111 16112 if (start) { 16113 pos = str.lastIndexOf(' ', offset); 16114 pos2 = str.lastIndexOf('\u00a0', offset); 16115 pos = pos > pos2 ? pos : pos2; 16116 16117 // Include the space on remove to avoid tag soup 16118 if (pos !== -1 && !remove) { 16119 pos++; 16120 } 16121 } else { 16122 pos = str.indexOf(' ', offset); 16123 pos2 = str.indexOf('\u00a0', offset); 16124 pos = pos !== -1 && (pos2 === -1 || pos < pos2) ? pos : pos2; 16125 } 16126 16127 return pos; 16128 } 16129 16130 if (container.nodeType === 3) { 16131 pos = findSpace(container, offset); 16132 16133 if (pos !== -1) { 16134 return {container: container, offset: pos}; 16135 } 16136 16137 lastTextNode = container; 16138 } 16139 16140 // Walk the nodes inside the block 16141 walker = new TreeWalker(container, dom.getParent(container, isBlock) || ed.getBody()); 16142 while ((node = walker[start ? 'prev' : 'next']())) { 16143 if (node.nodeType === 3) { 16144 lastTextNode = node; 16145 pos = findSpace(node); 16146 16147 if (pos !== -1) { 16148 return {container: node, offset: pos}; 16149 } 16150 } else if (isBlock(node)) { 16151 break; 16152 } 16153 } 16154 16155 if (lastTextNode) { 16156 if (start) { 16157 offset = 0; 16158 } else { 16159 offset = lastTextNode.length; 16160 } 16161 16162 return {container: lastTextNode, offset: offset}; 16163 } 16164 } 16165 16166 function findSelectorEndPoint(container, sibling_name) { 16167 var parents, i, y, curFormat; 16168 16169 if (container.nodeType == 3 && container.nodeValue.length === 0 && container[sibling_name]) { 16170 container = container[sibling_name]; 16171 } 16172 16173 parents = getParents(container); 16174 for (i = 0; i < parents.length; i++) { 16175 for (y = 0; y < format.length; y++) { 16176 curFormat = format[y]; 16177 16178 // If collapsed state is set then skip formats that doesn't match that 16179 if ("collapsed" in curFormat && curFormat.collapsed !== rng.collapsed) { 16180 continue; 16181 } 16182 16183 if (dom.is(parents[i], curFormat.selector)) { 16184 return parents[i]; 16185 } 16186 } 16187 } 16188 16189 return container; 16190 } 16191 16192 function findBlockEndPoint(container, sibling_name) { 16193 var node, root = dom.getRoot(); 16194 16195 // Expand to block of similar type 16196 if (!format[0].wrapper) { 16197 node = dom.getParent(container, format[0].block, root); 16198 } 16199 16200 // Expand to first wrappable block element or any block element 16201 if (!node) { 16202 node = dom.getParent(container.nodeType == 3 ? container.parentNode : container, function(node) { 16203 // Fixes #6183 where it would expand to editable parent element in inline mode 16204 return node != root && isTextBlock(node); 16205 }); 16206 } 16207 16208 // Exclude inner lists from wrapping 16209 if (node && format[0].wrapper) { 16210 node = getParents(node, 'ul,ol').reverse()[0] || node; 16211 } 16212 16213 // Didn't find a block element look for first/last wrappable element 16214 if (!node) { 16215 node = container; 16216 16217 while (node[sibling_name] && !isBlock(node[sibling_name])) { 16218 node = node[sibling_name]; 16219 16220 // Break on BR but include it will be removed later on 16221 // we can't remove it now since we need to check if it can be wrapped 16222 if (isEq(node, 'br')) { 16223 break; 16224 } 16225 } 16226 } 16227 16228 return node || container; 16229 } 16230 16231 // Expand to closest contentEditable element 16232 startContainer = findParentContentEditable(startContainer); 16233 endContainer = findParentContentEditable(endContainer); 16234 16235 // Exclude bookmark nodes if possible 16236 if (isBookmarkNode(startContainer.parentNode) || isBookmarkNode(startContainer)) { 16237 startContainer = isBookmarkNode(startContainer) ? startContainer : startContainer.parentNode; 16238 startContainer = startContainer.nextSibling || startContainer; 16239 16240 if (startContainer.nodeType == 3) { 16241 startOffset = 0; 16242 } 16243 } 16244 16245 if (isBookmarkNode(endContainer.parentNode) || isBookmarkNode(endContainer)) { 16246 endContainer = isBookmarkNode(endContainer) ? endContainer : endContainer.parentNode; 16247 endContainer = endContainer.previousSibling || endContainer; 16248 16249 if (endContainer.nodeType == 3) { 16250 endOffset = endContainer.length; 16251 } 16252 } 16253 16254 if (format[0].inline) { 16255 if (rng.collapsed) { 16256 // Expand left to closest word boundary 16257 endPoint = findWordEndPoint(startContainer, startOffset, true); 16258 if (endPoint) { 16259 startContainer = endPoint.container; 16260 startOffset = endPoint.offset; 16261 } 16262 16263 // Expand right to closest word boundary 16264 endPoint = findWordEndPoint(endContainer, endOffset); 16265 if (endPoint) { 16266 endContainer = endPoint.container; 16267 endOffset = endPoint.offset; 16268 } 16269 } 16270 16271 // Avoid applying formatting to a trailing space. 16272 leaf = findLeaf(endContainer, endOffset); 16273 if (leaf.node) { 16274 while (leaf.node && leaf.offset === 0 && leaf.node.previousSibling) { 16275 leaf = findLeaf(leaf.node.previousSibling); 16276 } 16277 16278 if (leaf.node && leaf.offset > 0 && leaf.node.nodeType === 3 && 16279 leaf.node.nodeValue.charAt(leaf.offset - 1) === ' ') { 16280 16281 if (leaf.offset > 1) { 16282 endContainer = leaf.node; 16283 endContainer.splitText(leaf.offset - 1); 16284 } 16285 } 16286 } 16287 } 16288 16289 // Move start/end point up the tree if the leaves are sharp and if we are in different containers 16290 // Example * becomes !: !<p><b><i>*text</i><i>text*</i></b></p>! 16291 // This will reduce the number of wrapper elements that needs to be created 16292 // Move start point up the tree 16293 if (format[0].inline || format[0].block_expand) { 16294 if (!format[0].inline || (startContainer.nodeType != 3 || startOffset === 0)) { 16295 startContainer = findParentContainer(true); 16296 } 16297 16298 if (!format[0].inline || (endContainer.nodeType != 3 || endOffset === endContainer.nodeValue.length)) { 16299 endContainer = findParentContainer(); 16300 } 16301 } 16302 16303 // Expand start/end container to matching selector 16304 if (format[0].selector && format[0].expand !== FALSE && !format[0].inline) { 16305 // Find new startContainer/endContainer if there is better one 16306 startContainer = findSelectorEndPoint(startContainer, 'previousSibling'); 16307 endContainer = findSelectorEndPoint(endContainer, 'nextSibling'); 16308 } 16309 16310 // Expand start/end container to matching block element or text node 16311 if (format[0].block || format[0].selector) { 16312 // Find new startContainer/endContainer if there is better one 16313 startContainer = findBlockEndPoint(startContainer, 'previousSibling'); 16314 endContainer = findBlockEndPoint(endContainer, 'nextSibling'); 16315 16316 // Non block element then try to expand up the leaf 16317 if (format[0].block) { 16318 if (!isBlock(startContainer)) { 16319 startContainer = findParentContainer(true); 16320 } 16321 16322 if (!isBlock(endContainer)) { 16323 endContainer = findParentContainer(); 16324 } 16325 } 16326 } 16327 16328 // Setup index for startContainer 16329 if (startContainer.nodeType == 1) { 16330 startOffset = nodeIndex(startContainer); 16331 startContainer = startContainer.parentNode; 16332 } 16333 16334 // Setup index for endContainer 16335 if (endContainer.nodeType == 1) { 16336 endOffset = nodeIndex(endContainer) + 1; 16337 endContainer = endContainer.parentNode; 16338 } 16339 16340 // Return new range like object 16341 return { 16342 startContainer: startContainer, 16343 startOffset: startOffset, 16344 endContainer: endContainer, 16345 endOffset: endOffset 16346 }; 16347 } 16348 16349 /** 16350 * Removes the specified format for the specified node. It will also remove the node if it doesn't have 16351 * any attributes if the format specifies it to do so. 16352 * 16353 * @private 16354 * @param {Object} format Format object with items to remove from node. 16355 * @param {Object} vars Name/value object with variables to apply to format. 16356 * @param {Node} node Node to remove the format styles on. 16357 * @param {Node} compare_node Optional compare node, if specified the styles will be compared to that node. 16358 * @return {Boolean} True/false if the node was removed or not. 16359 */ 16360 function removeFormat(format, vars, node, compare_node) { 16361 var i, attrs, stylesModified; 16362 16363 // Check if node matches format 16364 if (!matchName(node, format)) { 16365 return FALSE; 16366 } 16367 16368 // Should we compare with format attribs and styles 16369 if (format.remove != 'all') { 16370 // Remove styles 16371 each(format.styles, function(value, name) { 16372 value = normalizeStyleValue(replaceVars(value, vars), name); 16373 16374 // Indexed array 16375 if (typeof(name) === 'number') { 16376 name = value; 16377 compare_node = 0; 16378 } 16379 16380 if (!compare_node || isEq(getStyle(compare_node, name), value)) { 16381 dom.setStyle(node, name, ''); 16382 } 16383 16384 stylesModified = 1; 16385 }); 16386 16387 // Remove style attribute if it's empty 16388 if (stylesModified && dom.getAttrib(node, 'style') === '') { 16389 node.removeAttribute('style'); 16390 node.removeAttribute('data-mce-style'); 16391 } 16392 16393 // Remove attributes 16394 each(format.attributes, function(value, name) { 16395 var valueOut; 16396 16397 value = replaceVars(value, vars); 16398 16399 // Indexed array 16400 if (typeof(name) === 'number') { 16401 name = value; 16402 compare_node = 0; 16403 } 16404 16405 if (!compare_node || isEq(dom.getAttrib(compare_node, name), value)) { 16406 // Keep internal classes 16407 if (name == 'class') { 16408 value = dom.getAttrib(node, name); 16409 if (value) { 16410 // Build new class value where everything is removed except the internal prefixed classes 16411 valueOut = ''; 16412 each(value.split(/\s+/), function(cls) { 16413 if (/mce\w+/.test(cls)) { 16414 valueOut += (valueOut ? ' ' : '') + cls; 16415 } 16416 }); 16417 16418 // We got some internal classes left 16419 if (valueOut) { 16420 dom.setAttrib(node, name, valueOut); 16421 return; 16422 } 16423 } 16424 } 16425 16426 // IE6 has a bug where the attribute doesn't get removed correctly 16427 if (name == "class") { 16428 node.removeAttribute('className'); 16429 } 16430 16431 // Remove mce prefixed attributes 16432 if (MCE_ATTR_RE.test(name)) { 16433 node.removeAttribute('data-mce-' + name); 16434 } 16435 16436 node.removeAttribute(name); 16437 } 16438 }); 16439 16440 // Remove classes 16441 each(format.classes, function(value) { 16442 value = replaceVars(value, vars); 16443 16444 if (!compare_node || dom.hasClass(compare_node, value)) { 16445 dom.removeClass(node, value); 16446 } 16447 }); 16448 16449 // Check for non internal attributes 16450 attrs = dom.getAttribs(node); 16451 for (i = 0; i < attrs.length; i++) { 16452 if (attrs[i].nodeName.indexOf('_') !== 0) { 16453 return FALSE; 16454 } 16455 } 16456 } 16457 16458 // Remove the inline child if it's empty for example <b> or <span> 16459 if (format.remove != 'none') { 16460 removeNode(node, format); 16461 return TRUE; 16462 } 16463 } 16464 16465 /** 16466 * Removes the node and wrap it's children in paragraphs before doing so or 16467 * appends BR elements to the beginning/end of the block element if forcedRootBlocks is disabled. 16468 * 16469 * If the div in the node below gets removed: 16470 * text<div>text</div>text 16471 * 16472 * Output becomes: 16473 * text<div><br />text<br /></div>text 16474 * 16475 * So when the div is removed the result is: 16476 * text<br />text<br />text 16477 * 16478 * @private 16479 * @param {Node} node Node to remove + apply BR/P elements to. 16480 * @param {Object} format Format rule. 16481 * @return {Node} Input node. 16482 */ 16483 function removeNode(node, format) { 16484 var parentNode = node.parentNode, rootBlockElm; 16485 16486 function find(node, next, inc) { 16487 node = getNonWhiteSpaceSibling(node, next, inc); 16488 16489 return !node || (node.nodeName == 'BR' || isBlock(node)); 16490 } 16491 16492 if (format.block) { 16493 if (!forcedRootBlock) { 16494 // Append BR elements if needed before we remove the block 16495 if (isBlock(node) && !isBlock(parentNode)) { 16496 if (!find(node, FALSE) && !find(node.firstChild, TRUE, 1)) { 16497 node.insertBefore(dom.create('br'), node.firstChild); 16498 } 16499 16500 if (!find(node, TRUE) && !find(node.lastChild, FALSE, 1)) { 16501 node.appendChild(dom.create('br')); 16502 } 16503 } 16504 } else { 16505 // Wrap the block in a forcedRootBlock if we are at the root of document 16506 if (parentNode == dom.getRoot()) { 16507 if (!format.list_block || !isEq(node, format.list_block)) { 16508 each(grep(node.childNodes), function(node) { 16509 if (isValid(forcedRootBlock, node.nodeName.toLowerCase())) { 16510 if (!rootBlockElm) { 16511 rootBlockElm = wrap(node, forcedRootBlock); 16512 dom.setAttribs(rootBlockElm, ed.settings.forced_root_block_attrs); 16513 } else { 16514 rootBlockElm.appendChild(node); 16515 } 16516 } else { 16517 rootBlockElm = 0; 16518 } 16519 }); 16520 } 16521 } 16522 } 16523 } 16524 16525 // Never remove nodes that isn't the specified inline element if a selector is specified too 16526 if (format.selector && format.inline && !isEq(format.inline, node)) { 16527 return; 16528 } 16529 16530 dom.remove(node, 1); 16531 } 16532 16533 /** 16534 * Returns the next/previous non whitespace node. 16535 * 16536 * @private 16537 * @param {Node} node Node to start at. 16538 * @param {boolean} next (Optional) Include next or previous node defaults to previous. 16539 * @param {boolean} inc (Optional) Include the current node in checking. Defaults to false. 16540 * @return {Node} Next or previous node or undefined if it wasn't found. 16541 */ 16542 function getNonWhiteSpaceSibling(node, next, inc) { 16543 if (node) { 16544 next = next ? 'nextSibling' : 'previousSibling'; 16545 16546 for (node = inc ? node : node[next]; node; node = node[next]) { 16547 if (node.nodeType == 1 || !isWhiteSpaceNode(node)) { 16548 return node; 16549 } 16550 } 16551 } 16552 } 16553 16554 /** 16555 * Checks if the specified node is a bookmark node or not. 16556 * 16557 * @private 16558 * @param {Node} node Node to check if it's a bookmark node or not. 16559 * @return {Boolean} true/false if the node is a bookmark node. 16560 */ 16561 function isBookmarkNode(node) { 16562 return node && node.nodeType == 1 && node.getAttribute('data-mce-type') == 'bookmark'; 16563 } 16564 16565 /** 16566 * Merges the next/previous sibling element if they match. 16567 * 16568 * @private 16569 * @param {Node} prev Previous node to compare/merge. 16570 * @param {Node} next Next node to compare/merge. 16571 * @return {Node} Next node if we didn't merge and prev node if we did. 16572 */ 16573 function mergeSiblings(prev, next) { 16574 var sibling, tmpSibling; 16575 16576 /** 16577 * Compares two nodes and checks if it's attributes and styles matches. 16578 * This doesn't compare classes as items since their order is significant. 16579 * 16580 * @private 16581 * @param {Node} node1 First node to compare with. 16582 * @param {Node} node2 Second node to compare with. 16583 * @return {boolean} True/false if the nodes are the same or not. 16584 */ 16585 function compareElements(node1, node2) { 16586 // Not the same name 16587 if (node1.nodeName != node2.nodeName) { 16588 return FALSE; 16589 } 16590 16591 /** 16592 * Returns all the nodes attributes excluding internal ones, styles and classes. 16593 * 16594 * @private 16595 * @param {Node} node Node to get attributes from. 16596 * @return {Object} Name/value object with attributes and attribute values. 16597 */ 16598 function getAttribs(node) { 16599 var attribs = {}; 16600 16601 each(dom.getAttribs(node), function(attr) { 16602 var name = attr.nodeName.toLowerCase(); 16603 16604 // Don't compare internal attributes or style 16605 if (name.indexOf('_') !== 0 && name !== 'style' && name !== 'data-mce-style') { 16606 attribs[name] = dom.getAttrib(node, name); 16607 } 16608 }); 16609 16610 return attribs; 16611 } 16612 16613 /** 16614 * Compares two objects checks if it's key + value exists in the other one. 16615 * 16616 * @private 16617 * @param {Object} obj1 First object to compare. 16618 * @param {Object} obj2 Second object to compare. 16619 * @return {boolean} True/false if the objects matches or not. 16620 */ 16621 function compareObjects(obj1, obj2) { 16622 var value, name; 16623 16624 for (name in obj1) { 16625 // Obj1 has item obj2 doesn't have 16626 if (obj1.hasOwnProperty(name)) { 16627 value = obj2[name]; 16628 16629 // Obj2 doesn't have obj1 item 16630 if (value === undef) { 16631 return FALSE; 16632 } 16633 16634 // Obj2 item has a different value 16635 if (obj1[name] != value) { 16636 return FALSE; 16637 } 16638 16639 // Delete similar value 16640 delete obj2[name]; 16641 } 16642 } 16643 16644 // Check if obj 2 has something obj 1 doesn't have 16645 for (name in obj2) { 16646 // Obj2 has item obj1 doesn't have 16647 if (obj2.hasOwnProperty(name)) { 16648 return FALSE; 16649 } 16650 } 16651 16652 return TRUE; 16653 } 16654 16655 // Attribs are not the same 16656 if (!compareObjects(getAttribs(node1), getAttribs(node2))) { 16657 return FALSE; 16658 } 16659 16660 // Styles are not the same 16661 if (!compareObjects(dom.parseStyle(dom.getAttrib(node1, 'style')), dom.parseStyle(dom.getAttrib(node2, 'style')))) { 16662 return FALSE; 16663 } 16664 16665 return !isBookmarkNode(node1) && !isBookmarkNode(node2); 16666 } 16667 16668 function findElementSibling(node, sibling_name) { 16669 for (sibling = node; sibling; sibling = sibling[sibling_name]) { 16670 if (sibling.nodeType == 3 && sibling.nodeValue.length !== 0) { 16671 return node; 16672 } 16673 16674 if (sibling.nodeType == 1 && !isBookmarkNode(sibling)) { 16675 return sibling; 16676 } 16677 } 16678 16679 return node; 16680 } 16681 16682 // Check if next/prev exists and that they are elements 16683 if (prev && next) { 16684 // If previous sibling is empty then jump over it 16685 prev = findElementSibling(prev, 'previousSibling'); 16686 next = findElementSibling(next, 'nextSibling'); 16687 16688 // Compare next and previous nodes 16689 if (compareElements(prev, next)) { 16690 // Append nodes between 16691 for (sibling = prev.nextSibling; sibling && sibling != next;) { 16692 tmpSibling = sibling; 16693 sibling = sibling.nextSibling; 16694 prev.appendChild(tmpSibling); 16695 } 16696 16697 // Remove next node 16698 dom.remove(next); 16699 16700 // Move children into prev node 16701 each(grep(next.childNodes), function(node) { 16702 prev.appendChild(node); 16703 }); 16704 16705 return prev; 16706 } 16707 } 16708 16709 return next; 16710 } 16711 16712 function getContainer(rng, start) { 16713 var container, offset, lastIdx; 16714 16715 container = rng[start ? 'startContainer' : 'endContainer']; 16716 offset = rng[start ? 'startOffset' : 'endOffset']; 16717 16718 if (container.nodeType == 1) { 16719 lastIdx = container.childNodes.length - 1; 16720 16721 if (!start && offset) { 16722 offset--; 16723 } 16724 16725 container = container.childNodes[offset > lastIdx ? lastIdx : offset]; 16726 } 16727 16728 // If start text node is excluded then walk to the next node 16729 if (container.nodeType === 3 && start && offset >= container.nodeValue.length) { 16730 container = new TreeWalker(container, ed.getBody()).next() || container; 16731 } 16732 16733 // If end text node is excluded then walk to the previous node 16734 if (container.nodeType === 3 && !start && offset === 0) { 16735 container = new TreeWalker(container, ed.getBody()).prev() || container; 16736 } 16737 16738 return container; 16739 } 16740 16741 function performCaretAction(type, name, vars) { 16742 var caretContainerId = '_mce_caret', debug = ed.settings.caret_debug; 16743 16744 // Creates a caret container bogus element 16745 function createCaretContainer(fill) { 16746 var caretContainer = dom.create('span', {id: caretContainerId, 'data-mce-bogus': true, style: debug ? 'color:red' : ''}); 16747 16748 if (fill) { 16749 caretContainer.appendChild(ed.getDoc().createTextNode(INVISIBLE_CHAR)); 16750 } 16751 16752 return caretContainer; 16753 } 16754 16755 function isCaretContainerEmpty(node, nodes) { 16756 while (node) { 16757 if ((node.nodeType === 3 && node.nodeValue !== INVISIBLE_CHAR) || node.childNodes.length > 1) { 16758 return false; 16759 } 16760 16761 // Collect nodes 16762 if (nodes && node.nodeType === 1) { 16763 nodes.push(node); 16764 } 16765 16766 node = node.firstChild; 16767 } 16768 16769 return true; 16770 } 16771 16772 // Returns any parent caret container element 16773 function getParentCaretContainer(node) { 16774 while (node) { 16775 if (node.id === caretContainerId) { 16776 return node; 16777 } 16778 16779 node = node.parentNode; 16780 } 16781 } 16782 16783 // Finds the first text node in the specified node 16784 function findFirstTextNode(node) { 16785 var walker; 16786 16787 if (node) { 16788 walker = new TreeWalker(node, node); 16789 16790 for (node = walker.current(); node; node = walker.next()) { 16791 if (node.nodeType === 3) { 16792 return node; 16793 } 16794 } 16795 } 16796 } 16797 16798 // Removes the caret container for the specified node or all on the current document 16799 function removeCaretContainer(node, move_caret) { 16800 var child, rng; 16801 16802 if (!node) { 16803 node = getParentCaretContainer(selection.getStart()); 16804 16805 if (!node) { 16806 while ((node = dom.get(caretContainerId))) { 16807 removeCaretContainer(node, false); 16808 } 16809 } 16810 } else { 16811 rng = selection.getRng(true); 16812 16813 if (isCaretContainerEmpty(node)) { 16814 if (move_caret !== false) { 16815 rng.setStartBefore(node); 16816 rng.setEndBefore(node); 16817 } 16818 16819 dom.remove(node); 16820 } else { 16821 child = findFirstTextNode(node); 16822 16823 if (child.nodeValue.charAt(0) === INVISIBLE_CHAR) { 16824 child = child.deleteData(0, 1); 16825 } 16826 16827 dom.remove(node, 1); 16828 } 16829 16830 selection.setRng(rng); 16831 } 16832 } 16833 16834 // Applies formatting to the caret postion 16835 function applyCaretFormat() { 16836 var rng, caretContainer, textNode, offset, bookmark, container, text; 16837 16838 rng = selection.getRng(true); 16839 offset = rng.startOffset; 16840 container = rng.startContainer; 16841 text = container.nodeValue; 16842 16843 caretContainer = getParentCaretContainer(selection.getStart()); 16844 if (caretContainer) { 16845 textNode = findFirstTextNode(caretContainer); 16846 } 16847 16848 // Expand to word is caret is in the middle of a text node and the char before/after is a alpha numeric character 16849 if (text && offset > 0 && offset < text.length && /\w/.test(text.charAt(offset)) && /\w/.test(text.charAt(offset - 1))) { 16850 // Get bookmark of caret position 16851 bookmark = selection.getBookmark(); 16852 16853 // Collapse bookmark range (WebKit) 16854 rng.collapse(true); 16855 16856 // Expand the range to the closest word and split it at those points 16857 rng = expandRng(rng, get(name)); 16858 rng = rangeUtils.split(rng); 16859 16860 // Apply the format to the range 16861 apply(name, vars, rng); 16862 16863 // Move selection back to caret position 16864 selection.moveToBookmark(bookmark); 16865 } else { 16866 if (!caretContainer || textNode.nodeValue !== INVISIBLE_CHAR) { 16867 caretContainer = createCaretContainer(true); 16868 textNode = caretContainer.firstChild; 16869 16870 rng.insertNode(caretContainer); 16871 offset = 1; 16872 16873 apply(name, vars, caretContainer); 16874 } else { 16875 apply(name, vars, caretContainer); 16876 } 16877 16878 // Move selection to text node 16879 selection.setCursorLocation(textNode, offset); 16880 } 16881 } 16882 16883 function removeCaretFormat() { 16884 var rng = selection.getRng(true), container, offset, bookmark, 16885 hasContentAfter, node, formatNode, parents = [], i, caretContainer; 16886 16887 container = rng.startContainer; 16888 offset = rng.startOffset; 16889 node = container; 16890 16891 if (container.nodeType == 3) { 16892 if (offset != container.nodeValue.length || container.nodeValue === INVISIBLE_CHAR) { 16893 hasContentAfter = true; 16894 } 16895 16896 node = node.parentNode; 16897 } 16898 16899 while (node) { 16900 if (matchNode(node, name, vars)) { 16901 formatNode = node; 16902 break; 16903 } 16904 16905 if (node.nextSibling) { 16906 hasContentAfter = true; 16907 } 16908 16909 parents.push(node); 16910 node = node.parentNode; 16911 } 16912 16913 // Node doesn't have the specified format 16914 if (!formatNode) { 16915 return; 16916 } 16917 16918 // Is there contents after the caret then remove the format on the element 16919 if (hasContentAfter) { 16920 // Get bookmark of caret position 16921 bookmark = selection.getBookmark(); 16922 16923 // Collapse bookmark range (WebKit) 16924 rng.collapse(true); 16925 16926 // Expand the range to the closest word and split it at those points 16927 rng = expandRng(rng, get(name), true); 16928 rng = rangeUtils.split(rng); 16929 16930 // Remove the format from the range 16931 remove(name, vars, rng); 16932 16933 // Move selection back to caret position 16934 selection.moveToBookmark(bookmark); 16935 } else { 16936 caretContainer = createCaretContainer(); 16937 16938 node = caretContainer; 16939 for (i = parents.length - 1; i >= 0; i--) { 16940 node.appendChild(dom.clone(parents[i], false)); 16941 node = node.firstChild; 16942 } 16943 16944 // Insert invisible character into inner most format element 16945 node.appendChild(dom.doc.createTextNode(INVISIBLE_CHAR)); 16946 node = node.firstChild; 16947 16948 var block = dom.getParent(formatNode, isTextBlock); 16949 16950 if (block && dom.isEmpty(block)) { 16951 // Replace formatNode with caretContainer when removing format from empty block like <p><b>|</b></p> 16952 formatNode.parentNode.replaceChild(caretContainer, formatNode); 16953 } else { 16954 // Insert caret container after the formated node 16955 dom.insertAfter(caretContainer, formatNode); 16956 } 16957 16958 // Move selection to text node 16959 selection.setCursorLocation(node, 1); 16960 16961 // If the formatNode is empty, we can remove it safely. 16962 if (dom.isEmpty(formatNode)) { 16963 dom.remove(formatNode); 16964 } 16965 } 16966 } 16967 16968 // Checks if the parent caret container node isn't empty if that is the case it 16969 // will remove the bogus state on all children that isn't empty 16970 function unmarkBogusCaretParents() { 16971 var caretContainer; 16972 16973 caretContainer = getParentCaretContainer(selection.getStart()); 16974 if (caretContainer && !dom.isEmpty(caretContainer)) { 16975 walk(caretContainer, function(node) { 16976 if (node.nodeType == 1 && node.id !== caretContainerId && !dom.isEmpty(node)) { 16977 dom.setAttrib(node, 'data-mce-bogus', null); 16978 } 16979 }, 'childNodes'); 16980 } 16981 } 16982 16983 // Only bind the caret events once 16984 if (!ed._hasCaretEvents) { 16985 // Mark current caret container elements as bogus when getting the contents so we don't end up with empty elements 16986 markCaretContainersBogus = function() { 16987 var nodes = [], i; 16988 16989 if (isCaretContainerEmpty(getParentCaretContainer(selection.getStart()), nodes)) { 16990 // Mark children 16991 i = nodes.length; 16992 while (i--) { 16993 dom.setAttrib(nodes[i], 'data-mce-bogus', '1'); 16994 } 16995 } 16996 }; 16997 16998 disableCaretContainer = function(e) { 16999 var keyCode = e.keyCode; 17000 17001 removeCaretContainer(); 17002 17003 // Remove caret container on keydown and it's a backspace, enter or left/right arrow keys 17004 if (keyCode == 8 || keyCode == 37 || keyCode == 39) { 17005 removeCaretContainer(getParentCaretContainer(selection.getStart())); 17006 } 17007 17008 unmarkBogusCaretParents(); 17009 }; 17010 17011 // Remove bogus state if they got filled by contents using editor.selection.setContent 17012 ed.on('SetContent', function(e) { 17013 if (e.selection) { 17014 unmarkBogusCaretParents(); 17015 } 17016 }); 17017 ed._hasCaretEvents = true; 17018 } 17019 17020 // Do apply or remove caret format 17021 if (type == "apply") { 17022 applyCaretFormat(); 17023 } else { 17024 removeCaretFormat(); 17025 } 17026 } 17027 17028 /** 17029 * Moves the start to the first suitable text node. 17030 */ 17031 function moveStart(rng) { 17032 var container = rng.startContainer, 17033 offset = rng.startOffset, isAtEndOfText, 17034 walker, node, nodes, tmpNode; 17035 17036 // Convert text node into index if possible 17037 if (container.nodeType == 3 && offset >= container.nodeValue.length) { 17038 // Get the parent container location and walk from there 17039 offset = nodeIndex(container); 17040 container = container.parentNode; 17041 isAtEndOfText = true; 17042 } 17043 17044 // Move startContainer/startOffset in to a suitable node 17045 if (container.nodeType == 1) { 17046 nodes = container.childNodes; 17047 container = nodes[Math.min(offset, nodes.length - 1)]; 17048 walker = new TreeWalker(container, dom.getParent(container, dom.isBlock)); 17049 17050 // If offset is at end of the parent node walk to the next one 17051 if (offset > nodes.length - 1 || isAtEndOfText) { 17052 walker.next(); 17053 } 17054 17055 for (node = walker.current(); node; node = walker.next()) { 17056 if (node.nodeType == 3 && !isWhiteSpaceNode(node)) { 17057 // IE has a "neat" feature where it moves the start node into the closest element 17058 // we can avoid this by inserting an element before it and then remove it after we set the selection 17059 tmpNode = dom.create('a', null, INVISIBLE_CHAR); 17060 node.parentNode.insertBefore(tmpNode, node); 17061 17062 // Set selection and remove tmpNode 17063 rng.setStart(node, 0); 17064 selection.setRng(rng); 17065 dom.remove(tmpNode); 17066 17067 return; 17068 } 17069 } 17070 } 17071 } 17072 }; 17073 }); 17074 17075 // Included from: js/tinymce/classes/UndoManager.js 17076 17077 /** 17078 * UndoManager.js 17079 * 17080 * Copyright, Moxiecode Systems AB 17081 * Released under LGPL License. 17082 * 17083 * License: http://www.tinymce.com/license 17084 * Contributing: http://www.tinymce.com/contributing 17085 */ 17086 17087 /** 17088 * This class handles the undo/redo history levels for the editor. Since the build in undo/redo has major drawbacks a custom one was needed. 17089 * 17090 * @class tinymce.UndoManager 17091 */ 17092 define("tinymce/UndoManager", [ 17093 "tinymce/Env", 17094 "tinymce/util/Tools" 17095 ], function(Env, Tools) { 17096 var trim = Tools.trim, trimContentRegExp; 17097 17098 trimContentRegExp = new RegExp([ 17099 '<span[^>]+data-mce-bogus[^>]+>[\u200B\uFEFF]+<\\/span>', // Trim bogus spans like caret containers 17100 '<div[^>]+data-mce-bogus[^>]+><\\/div>', // Trim bogus divs like resize handles 17101 '\\s?data-mce-selected="[^"]+"' // Trim temporaty data-mce prefixed attributes like data-mce-selected 17102 ].join('|'), 'gi'); 17103 17104 return function(editor) { 17105 var self = this, index = 0, data = [], beforeBookmark, isFirstTypedCharacter, locks = 0; 17106 17107 // Returns a trimmed version of the current editor contents 17108 function getContent() { 17109 return trim(editor.getContent({format: 'raw', no_events: 1}).replace(trimContentRegExp, '')); 17110 } 17111 17112 function addNonTypingUndoLevel(e) { 17113 self.typing = false; 17114 self.add({}, e); 17115 } 17116 17117 // Add initial undo level when the editor is initialized 17118 editor.on('init', function() { 17119 self.add(); 17120 }); 17121 17122 // Get position before an execCommand is processed 17123 editor.on('BeforeExecCommand', function(e) { 17124 var cmd = e.command; 17125 17126 if (cmd != 'Undo' && cmd != 'Redo' && cmd != 'mceRepaint') { 17127 self.beforeChange(); 17128 } 17129 }); 17130 17131 // Add undo level after an execCommand call was made 17132 editor.on('ExecCommand', function(e) { 17133 var cmd = e.command; 17134 17135 if (cmd != 'Undo' && cmd != 'Redo' && cmd != 'mceRepaint') { 17136 addNonTypingUndoLevel(e); 17137 } 17138 }); 17139 17140 editor.on('ObjectResizeStart', function() { 17141 self.beforeChange(); 17142 }); 17143 17144 editor.on('SaveContent ObjectResized blur', addNonTypingUndoLevel); 17145 editor.on('DragEnd', addNonTypingUndoLevel); 17146 17147 editor.on('KeyUp', function(e) { 17148 var keyCode = e.keyCode; 17149 17150 if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode == 45 || keyCode == 13 || e.ctrlKey) { 17151 addNonTypingUndoLevel(); 17152 editor.nodeChanged(); 17153 } 17154 17155 if (keyCode == 46 || keyCode == 8 || (Env.mac && (keyCode == 91 || keyCode == 93))) { 17156 editor.nodeChanged(); 17157 } 17158 17159 // Fire a TypingUndo event on the first character entered 17160 if (isFirstTypedCharacter && self.typing) { 17161 // Make the it dirty if the content was changed after typing the first character 17162 if (!editor.isDirty()) { 17163 editor.isNotDirty = !data[0] || getContent() == data[0].content; 17164 17165 // Fire initial change event 17166 if (!editor.isNotDirty) { 17167 editor.fire('change', {level: data[0], lastLevel: null}); 17168 } 17169 } 17170 17171 editor.fire('TypingUndo'); 17172 isFirstTypedCharacter = false; 17173 editor.nodeChanged(); 17174 } 17175 }); 17176 17177 editor.on('KeyDown', function(e) { 17178 var keyCode = e.keyCode; 17179 17180 // Is caracter positon keys left,right,up,down,home,end,pgdown,pgup,enter 17181 if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode == 45) { 17182 if (self.typing) { 17183 addNonTypingUndoLevel(e); 17184 } 17185 17186 return; 17187 } 17188 17189 // If key isn't shift,ctrl,alt,capslock,metakey 17190 if ((keyCode < 16 || keyCode > 20) && keyCode != 224 && keyCode != 91 && !self.typing) { 17191 self.beforeChange(); 17192 self.typing = true; 17193 self.add({}, e); 17194 isFirstTypedCharacter = true; 17195 } 17196 }); 17197 17198 editor.on('MouseDown', function(e) { 17199 if (self.typing) { 17200 addNonTypingUndoLevel(e); 17201 } 17202 }); 17203 17204 // Add keyboard shortcuts for undo/redo keys 17205 editor.addShortcut('ctrl+z', '', 'Undo'); 17206 editor.addShortcut('ctrl+y,ctrl+shift+z', '', 'Redo'); 17207 17208 editor.on('AddUndo Undo Redo ClearUndos MouseUp', function(e) { 17209 if (!e.isDefaultPrevented()) { 17210 editor.nodeChanged(); 17211 } 17212 }); 17213 17214 self = { 17215 // Explose for debugging reasons 17216 data: data, 17217 17218 /** 17219 * State if the user is currently typing or not. This will add a typing operation into one undo 17220 * level instead of one new level for each keystroke. 17221 * 17222 * @field {Boolean} typing 17223 */ 17224 typing: false, 17225 17226 /** 17227 * Stores away a bookmark to be used when performing an undo action so that the selection is before 17228 * the change has been made. 17229 * 17230 * @method beforeChange 17231 */ 17232 beforeChange: function() { 17233 if (!locks) { 17234 beforeBookmark = editor.selection.getBookmark(2, true); 17235 } 17236 }, 17237 17238 /** 17239 * Adds a new undo level/snapshot to the undo list. 17240 * 17241 * @method add 17242 * @param {Object} level Optional undo level object to add. 17243 * @param {DOMEvent} Event Optional event responsible for the creation of the undo level. 17244 * @return {Object} Undo level that got added or null it a level wasn't needed. 17245 */ 17246 add: function(level, event) { 17247 var i, settings = editor.settings, lastLevel; 17248 17249 level = level || {}; 17250 level.content = getContent(); 17251 17252 if (locks || editor.removed) { 17253 return null; 17254 } 17255 17256 lastLevel = data[index]; 17257 if (editor.fire('BeforeAddUndo', {level: level, lastLevel: lastLevel, originalEvent: event}).isDefaultPrevented()) { 17258 return null; 17259 } 17260 17261 // Add undo level if needed 17262 if (lastLevel && lastLevel.content == level.content) { 17263 return null; 17264 } 17265 17266 // Set before bookmark on previous level 17267 if (data[index]) { 17268 data[index].beforeBookmark = beforeBookmark; 17269 } 17270 17271 // Time to compress 17272 if (settings.custom_undo_redo_levels) { 17273 if (data.length > settings.custom_undo_redo_levels) { 17274 for (i = 0; i < data.length - 1; i++) { 17275 data[i] = data[i + 1]; 17276 } 17277 17278 data.length--; 17279 index = data.length; 17280 } 17281 } 17282 17283 // Get a non intrusive normalized bookmark 17284 level.bookmark = editor.selection.getBookmark(2, true); 17285 17286 // Crop array if needed 17287 if (index < data.length - 1) { 17288 data.length = index + 1; 17289 } 17290 17291 data.push(level); 17292 index = data.length - 1; 17293 17294 var args = {level: level, lastLevel: lastLevel, originalEvent: event}; 17295 17296 editor.fire('AddUndo', args); 17297 17298 if (index > 0) { 17299 editor.isNotDirty = false; 17300 editor.fire('change', args); 17301 } 17302 17303 return level; 17304 }, 17305 17306 /** 17307 * Undoes the last action. 17308 * 17309 * @method undo 17310 * @return {Object} Undo level or null if no undo was performed. 17311 */ 17312 undo: function() { 17313 var level; 17314 17315 if (self.typing) { 17316 self.add(); 17317 self.typing = false; 17318 } 17319 17320 if (index > 0) { 17321 level = data[--index]; 17322 17323 // Undo to first index then set dirty state to false 17324 if (index === 0) { 17325 editor.isNotDirty = true; 17326 } 17327 17328 editor.setContent(level.content, {format: 'raw'}); 17329 editor.selection.moveToBookmark(level.beforeBookmark); 17330 17331 editor.fire('undo', {level: level}); 17332 } 17333 17334 return level; 17335 }, 17336 17337 /** 17338 * Redoes the last action. 17339 * 17340 * @method redo 17341 * @return {Object} Redo level or null if no redo was performed. 17342 */ 17343 redo: function() { 17344 var level; 17345 17346 if (index < data.length - 1) { 17347 level = data[++index]; 17348 17349 editor.setContent(level.content, {format: 'raw'}); 17350 editor.selection.moveToBookmark(level.bookmark); 17351 17352 editor.fire('redo', {level: level}); 17353 } 17354 17355 return level; 17356 }, 17357 17358 /** 17359 * Removes all undo levels. 17360 * 17361 * @method clear 17362 */ 17363 clear: function() { 17364 data = []; 17365 index = 0; 17366 self.typing = false; 17367 editor.fire('ClearUndos'); 17368 }, 17369 17370 /** 17371 * Returns true/false if the undo manager has any undo levels. 17372 * 17373 * @method hasUndo 17374 * @return {Boolean} true/false if the undo manager has any undo levels. 17375 */ 17376 hasUndo: function() { 17377 // Has undo levels or typing and content isn't the same as the initial level 17378 return index > 0 || (self.typing && data[0] && getContent() != data[0].content); 17379 }, 17380 17381 /** 17382 * Returns true/false if the undo manager has any redo levels. 17383 * 17384 * @method hasRedo 17385 * @return {Boolean} true/false if the undo manager has any redo levels. 17386 */ 17387 hasRedo: function() { 17388 return index < data.length - 1 && !this.typing; 17389 }, 17390 17391 /** 17392 * Executes the specified function in an undo transation. The selection 17393 * before the modification will be stored to the undo stack and if the DOM changes 17394 * it will add a new undo level. Any methods within the transation that adds undo levels will 17395 * be ignored. So a transation can include calls to execCommand or editor.insertContent. 17396 * 17397 * @method transact 17398 * @param {function} callback Function to execute dom manipulation logic in. 17399 */ 17400 transact: function(callback) { 17401 self.beforeChange(); 17402 17403 try { 17404 locks++; 17405 callback(); 17406 } finally { 17407 locks--; 17408 } 17409 17410 self.add(); 17411 } 17412 }; 17413 17414 return self; 17415 }; 17416 }); 17417 17418 // Included from: js/tinymce/classes/EnterKey.js 17419 17420 /** 17421 * EnterKey.js 17422 * 17423 * Copyright, Moxiecode Systems AB 17424 * Released under LGPL License. 17425 * 17426 * License: http://www.tinymce.com/license 17427 * Contributing: http://www.tinymce.com/contributing 17428 */ 17429 17430 /** 17431 * Contains logic for handling the enter key to split/generate block elements. 17432 */ 17433 define("tinymce/EnterKey", [ 17434 "tinymce/dom/TreeWalker", 17435 "tinymce/dom/RangeUtils", 17436 "tinymce/Env" 17437 ], function(TreeWalker, RangeUtils, Env) { 17438 var isIE = Env.ie && Env.ie < 11; 17439 17440 return function(editor) { 17441 var dom = editor.dom, selection = editor.selection, settings = editor.settings; 17442 var undoManager = editor.undoManager, schema = editor.schema, nonEmptyElementsMap = schema.getNonEmptyElements(); 17443 17444 function handleEnterKey(evt) { 17445 var rng, tmpRng, editableRoot, container, offset, parentBlock, documentMode, shiftKey, 17446 newBlock, fragment, containerBlock, parentBlockName, containerBlockName, newBlockName, isAfterLastNodeInContainer; 17447 17448 // Returns true if the block can be split into two blocks or not 17449 function canSplitBlock(node) { 17450 return node && 17451 dom.isBlock(node) && 17452 !/^(TD|TH|CAPTION|FORM)$/.test(node.nodeName) && 17453 !/^(fixed|absolute)/i.test(node.style.position) && 17454 dom.getContentEditable(node) !== "true"; 17455 } 17456 17457 // Renders empty block on IE 17458 function renderBlockOnIE(block) { 17459 var oldRng; 17460 17461 if (dom.isBlock(block)) { 17462 oldRng = selection.getRng(); 17463 block.appendChild(dom.create('span', null, '\u00a0')); 17464 selection.select(block); 17465 block.lastChild.outerHTML = ''; 17466 selection.setRng(oldRng); 17467 } 17468 } 17469 17470 // Remove the first empty inline element of the block so this: <p><b><em></em></b>x</p> becomes this: <p>x</p> 17471 function trimInlineElementsOnLeftSideOfBlock(block) { 17472 var node = block, firstChilds = [], i; 17473 17474 // Find inner most first child ex: <p><i><b>*</b></i></p> 17475 while ((node = node.firstChild)) { 17476 if (dom.isBlock(node)) { 17477 return; 17478 } 17479 17480 if (node.nodeType == 1 && !nonEmptyElementsMap[node.nodeName.toLowerCase()]) { 17481 firstChilds.push(node); 17482 } 17483 } 17484 17485 i = firstChilds.length; 17486 while (i--) { 17487 node = firstChilds[i]; 17488 if (!node.hasChildNodes() || (node.firstChild == node.lastChild && node.firstChild.nodeValue === '')) { 17489 dom.remove(node); 17490 } else { 17491 // Remove <a> </a> see #5381 17492 if (node.nodeName == "A" && (node.innerText || node.textContent) === ' ') { 17493 dom.remove(node); 17494 } 17495 } 17496 } 17497 } 17498 17499 // Moves the caret to a suitable position within the root for example in the first non 17500 // pure whitespace text node or before an image 17501 function moveToCaretPosition(root) { 17502 var walker, node, rng, lastNode = root, tempElm; 17503 17504 function firstNonWhiteSpaceNodeSibling(node) { 17505 while (node) { 17506 if (node.nodeType == 1 || (node.nodeType == 3 && node.data && /[\r\n\s]/.test(node.data))) { 17507 return node; 17508 } 17509 17510 node = node.nextSibling; 17511 } 17512 } 17513 17514 // Old IE versions doesn't properly render blocks with br elements in them 17515 // For example <p><br></p> wont be rendered correctly in a contentEditable area 17516 // until you remove the br producing <p></p> 17517 if (Env.ie && Env.ie < 9 && parentBlock && parentBlock.firstChild) { 17518 if (parentBlock.firstChild == parentBlock.lastChild && parentBlock.firstChild.tagName == 'BR') { 17519 dom.remove(parentBlock.firstChild); 17520 } 17521 } 17522 17523 if (root.nodeName == 'LI') { 17524 var firstChild = firstNonWhiteSpaceNodeSibling(root.firstChild); 17525 17526 if (firstChild && /^(UL|OL)$/.test(firstChild.nodeName)) { 17527 root.insertBefore(dom.doc.createTextNode('\u00a0'), root.firstChild); 17528 } 17529 } 17530 17531 rng = dom.createRng(); 17532 17533 if (root.hasChildNodes()) { 17534 walker = new TreeWalker(root, root); 17535 17536 while ((node = walker.current())) { 17537 if (node.nodeType == 3) { 17538 rng.setStart(node, 0); 17539 rng.setEnd(node, 0); 17540 break; 17541 } 17542 17543 if (nonEmptyElementsMap[node.nodeName.toLowerCase()]) { 17544 rng.setStartBefore(node); 17545 rng.setEndBefore(node); 17546 break; 17547 } 17548 17549 lastNode = node; 17550 node = walker.next(); 17551 } 17552 17553 if (!node) { 17554 rng.setStart(lastNode, 0); 17555 rng.setEnd(lastNode, 0); 17556 } 17557 } else { 17558 if (root.nodeName == 'BR') { 17559 if (root.nextSibling && dom.isBlock(root.nextSibling)) { 17560 // Trick on older IE versions to render the caret before the BR between two lists 17561 if (!documentMode || documentMode < 9) { 17562 tempElm = dom.create('br'); 17563 root.parentNode.insertBefore(tempElm, root); 17564 } 17565 17566 rng.setStartBefore(root); 17567 rng.setEndBefore(root); 17568 } else { 17569 rng.setStartAfter(root); 17570 rng.setEndAfter(root); 17571 } 17572 } else { 17573 rng.setStart(root, 0); 17574 rng.setEnd(root, 0); 17575 } 17576 } 17577 17578 selection.setRng(rng); 17579 17580 // Remove tempElm created for old IE:s 17581 dom.remove(tempElm); 17582 selection.scrollIntoView(root); 17583 } 17584 17585 function setForcedBlockAttrs(node) { 17586 var forcedRootBlockName = settings.forced_root_block; 17587 17588 if (forcedRootBlockName && forcedRootBlockName.toLowerCase() === node.tagName.toLowerCase()) { 17589 dom.setAttribs(node, settings.forced_root_block_attrs); 17590 } 17591 } 17592 17593 // Creates a new block element by cloning the current one or creating a new one if the name is specified 17594 // This function will also copy any text formatting from the parent block and add it to the new one 17595 function createNewBlock(name) { 17596 var node = container, block, clonedNode, caretNode; 17597 17598 if (name || parentBlockName == "TABLE") { 17599 block = dom.create(name || newBlockName); 17600 setForcedBlockAttrs(block); 17601 } else { 17602 block = parentBlock.cloneNode(false); 17603 } 17604 17605 caretNode = block; 17606 17607 // Clone any parent styles 17608 if (settings.keep_styles !== false) { 17609 do { 17610 if (/^(SPAN|STRONG|B|EM|I|FONT|STRIKE|U|VAR|CITE|DFN|CODE|MARK|Q|SUP|SUB|SAMP)$/.test(node.nodeName)) { 17611 // Never clone a caret containers 17612 if (node.id == '_mce_caret') { 17613 continue; 17614 } 17615 17616 clonedNode = node.cloneNode(false); 17617 dom.setAttrib(clonedNode, 'id', ''); // Remove ID since it needs to be document unique 17618 17619 if (block.hasChildNodes()) { 17620 clonedNode.appendChild(block.firstChild); 17621 block.appendChild(clonedNode); 17622 } else { 17623 caretNode = clonedNode; 17624 block.appendChild(clonedNode); 17625 } 17626 } 17627 } while ((node = node.parentNode)); 17628 } 17629 17630 // BR is needed in empty blocks on non IE browsers 17631 if (!isIE) { 17632 caretNode.innerHTML = '<br data-mce-bogus="1">'; 17633 } 17634 17635 return block; 17636 } 17637 17638 // Returns true/false if the caret is at the start/end of the parent block element 17639 function isCaretAtStartOrEndOfBlock(start) { 17640 var walker, node, name; 17641 17642 // Caret is in the middle of a text node like "a|b" 17643 if (container.nodeType == 3 && (start ? offset > 0 : offset < container.nodeValue.length)) { 17644 return false; 17645 } 17646 17647 // If after the last element in block node edge case for #5091 17648 if (container.parentNode == parentBlock && isAfterLastNodeInContainer && !start) { 17649 return true; 17650 } 17651 17652 // If the caret if before the first element in parentBlock 17653 if (start && container.nodeType == 1 && container == parentBlock.firstChild) { 17654 return true; 17655 } 17656 17657 // Caret can be before/after a table 17658 if (container.nodeName === "TABLE" || (container.previousSibling && container.previousSibling.nodeName == "TABLE")) { 17659 return (isAfterLastNodeInContainer && !start) || (!isAfterLastNodeInContainer && start); 17660 } 17661 17662 // Walk the DOM and look for text nodes or non empty elements 17663 walker = new TreeWalker(container, parentBlock); 17664 17665 // If caret is in beginning or end of a text block then jump to the next/previous node 17666 if (container.nodeType == 3) { 17667 if (start && offset === 0) { 17668 walker.prev(); 17669 } else if (!start && offset == container.nodeValue.length) { 17670 walker.next(); 17671 } 17672 } 17673 17674 while ((node = walker.current())) { 17675 if (node.nodeType === 1) { 17676 // Ignore bogus elements 17677 if (!node.getAttribute('data-mce-bogus')) { 17678 // Keep empty elements like <img /> <input /> but not trailing br:s like <p>text|<br></p> 17679 name = node.nodeName.toLowerCase(); 17680 if (nonEmptyElementsMap[name] && name !== 'br') { 17681 return false; 17682 } 17683 } 17684 } else if (node.nodeType === 3 && !/^[ \t\r\n]*$/.test(node.nodeValue)) { 17685 return false; 17686 } 17687 17688 if (start) { 17689 walker.prev(); 17690 } else { 17691 walker.next(); 17692 } 17693 } 17694 17695 return true; 17696 } 17697 17698 // Wraps any text nodes or inline elements in the specified forced root block name 17699 function wrapSelfAndSiblingsInDefaultBlock(container, offset) { 17700 var newBlock, parentBlock, startNode, node, next, rootBlockName, blockName = newBlockName || 'P'; 17701 17702 // Not in a block element or in a table cell or caption 17703 parentBlock = dom.getParent(container, dom.isBlock); 17704 rootBlockName = editor.getBody().nodeName.toLowerCase(); 17705 if (!parentBlock || !canSplitBlock(parentBlock)) { 17706 parentBlock = parentBlock || editableRoot; 17707 17708 if (!parentBlock.hasChildNodes()) { 17709 newBlock = dom.create(blockName); 17710 setForcedBlockAttrs(newBlock); 17711 parentBlock.appendChild(newBlock); 17712 rng.setStart(newBlock, 0); 17713 rng.setEnd(newBlock, 0); 17714 return newBlock; 17715 } 17716 17717 // Find parent that is the first child of parentBlock 17718 node = container; 17719 while (node.parentNode != parentBlock) { 17720 node = node.parentNode; 17721 } 17722 17723 // Loop left to find start node start wrapping at 17724 while (node && !dom.isBlock(node)) { 17725 startNode = node; 17726 node = node.previousSibling; 17727 } 17728 17729 if (startNode && schema.isValidChild(rootBlockName, blockName.toLowerCase())) { 17730 newBlock = dom.create(blockName); 17731 setForcedBlockAttrs(newBlock); 17732 startNode.parentNode.insertBefore(newBlock, startNode); 17733 17734 // Start wrapping until we hit a block 17735 node = startNode; 17736 while (node && !dom.isBlock(node)) { 17737 next = node.nextSibling; 17738 newBlock.appendChild(node); 17739 node = next; 17740 } 17741 17742 // Restore range to it's past location 17743 rng.setStart(container, offset); 17744 rng.setEnd(container, offset); 17745 } 17746 } 17747 17748 return container; 17749 } 17750 17751 // Inserts a block or br before/after or in the middle of a split list of the LI is empty 17752 function handleEmptyListItem() { 17753 function isFirstOrLastLi(first) { 17754 var node = containerBlock[first ? 'firstChild' : 'lastChild']; 17755 17756 // Find first/last element since there might be whitespace there 17757 while (node) { 17758 if (node.nodeType == 1) { 17759 break; 17760 } 17761 17762 node = node[first ? 'nextSibling' : 'previousSibling']; 17763 } 17764 17765 return node === parentBlock; 17766 } 17767 17768 function getContainerBlock() { 17769 var containerBlockParent = containerBlock.parentNode; 17770 17771 if (containerBlockParent.nodeName == 'LI') { 17772 return containerBlockParent; 17773 } 17774 17775 return containerBlock; 17776 } 17777 17778 // Check if we are in an nested list 17779 var containerBlockParentName = containerBlock.parentNode.nodeName; 17780 if (/^(OL|UL|LI)$/.test(containerBlockParentName)) { 17781 newBlockName = 'LI'; 17782 } 17783 17784 newBlock = newBlockName ? createNewBlock(newBlockName) : dom.create('BR'); 17785 17786 if (isFirstOrLastLi(true) && isFirstOrLastLi()) { 17787 if (containerBlockParentName == 'LI') { 17788 // Nested list is inside a LI 17789 dom.insertAfter(newBlock, getContainerBlock()); 17790 } else { 17791 // Is first and last list item then replace the OL/UL with a text block 17792 dom.replace(newBlock, containerBlock); 17793 } 17794 } else if (isFirstOrLastLi(true)) { 17795 if (containerBlockParentName == 'LI') { 17796 // List nested in an LI then move the list to a new sibling LI 17797 dom.insertAfter(newBlock, getContainerBlock()); 17798 newBlock.appendChild(dom.doc.createTextNode(' ')); // Needed for IE so the caret can be placed 17799 newBlock.appendChild(containerBlock); 17800 } else { 17801 // First LI in list then remove LI and add text block before list 17802 containerBlock.parentNode.insertBefore(newBlock, containerBlock); 17803 } 17804 } else if (isFirstOrLastLi()) { 17805 // Last LI in list then remove LI and add text block after list 17806 dom.insertAfter(newBlock, getContainerBlock()); 17807 renderBlockOnIE(newBlock); 17808 } else { 17809 // Middle LI in list the split the list and insert a text block in the middle 17810 // Extract after fragment and insert it after the current block 17811 containerBlock = getContainerBlock(); 17812 tmpRng = rng.cloneRange(); 17813 tmpRng.setStartAfter(parentBlock); 17814 tmpRng.setEndAfter(containerBlock); 17815 fragment = tmpRng.extractContents(); 17816 17817 if (newBlockName == 'LI' && fragment.firstChild.nodeName == 'LI') { 17818 newBlock = fragment.firstChild; 17819 dom.insertAfter(fragment, containerBlock); 17820 } else { 17821 dom.insertAfter(fragment, containerBlock); 17822 dom.insertAfter(newBlock, containerBlock); 17823 } 17824 } 17825 17826 dom.remove(parentBlock); 17827 moveToCaretPosition(newBlock); 17828 undoManager.add(); 17829 } 17830 17831 // Walks the parent block to the right and look for BR elements 17832 function hasRightSideContent() { 17833 var walker = new TreeWalker(container, parentBlock), node; 17834 17835 while ((node = walker.next())) { 17836 if (nonEmptyElementsMap[node.nodeName.toLowerCase()] || node.length > 0) { 17837 return true; 17838 } 17839 } 17840 } 17841 17842 // Inserts a BR element if the forced_root_block option is set to false or empty string 17843 function insertBr() { 17844 var brElm, extraBr, marker; 17845 17846 if (container && container.nodeType == 3 && offset >= container.nodeValue.length) { 17847 // Insert extra BR element at the end block elements 17848 if (!isIE && !hasRightSideContent()) { 17849 brElm = dom.create('br'); 17850 rng.insertNode(brElm); 17851 rng.setStartAfter(brElm); 17852 rng.setEndAfter(brElm); 17853 extraBr = true; 17854 } 17855 } 17856 17857 brElm = dom.create('br'); 17858 rng.insertNode(brElm); 17859 17860 // Rendering modes below IE8 doesn't display BR elements in PRE unless we have a \n before it 17861 if (isIE && parentBlockName == 'PRE' && (!documentMode || documentMode < 8)) { 17862 brElm.parentNode.insertBefore(dom.doc.createTextNode('\r'), brElm); 17863 } 17864 17865 // Insert temp marker and scroll to that 17866 marker = dom.create('span', {}, ' '); 17867 brElm.parentNode.insertBefore(marker, brElm); 17868 selection.scrollIntoView(marker); 17869 dom.remove(marker); 17870 17871 if (!extraBr) { 17872 rng.setStartAfter(brElm); 17873 rng.setEndAfter(brElm); 17874 } else { 17875 rng.setStartBefore(brElm); 17876 rng.setEndBefore(brElm); 17877 } 17878 17879 selection.setRng(rng); 17880 undoManager.add(); 17881 } 17882 17883 // Trims any linebreaks at the beginning of node user for example when pressing enter in a PRE element 17884 function trimLeadingLineBreaks(node) { 17885 do { 17886 if (node.nodeType === 3) { 17887 node.nodeValue = node.nodeValue.replace(/^[\r\n]+/, ''); 17888 } 17889 17890 node = node.firstChild; 17891 } while (node); 17892 } 17893 17894 function getEditableRoot(node) { 17895 var root = dom.getRoot(), parent, editableRoot; 17896 17897 // Get all parents until we hit a non editable parent or the root 17898 parent = node; 17899 while (parent !== root && dom.getContentEditable(parent) !== "false") { 17900 if (dom.getContentEditable(parent) === "true") { 17901 editableRoot = parent; 17902 } 17903 17904 parent = parent.parentNode; 17905 } 17906 17907 return parent !== root ? editableRoot : root; 17908 } 17909 17910 // Adds a BR at the end of blocks that only contains an IMG or INPUT since 17911 // these might be floated and then they won't expand the block 17912 function addBrToBlockIfNeeded(block) { 17913 var lastChild; 17914 17915 // IE will render the blocks correctly other browsers needs a BR 17916 if (!isIE) { 17917 block.normalize(); // Remove empty text nodes that got left behind by the extract 17918 17919 // Check if the block is empty or contains a floated last child 17920 lastChild = block.lastChild; 17921 if (!lastChild || (/^(left|right)$/gi.test(dom.getStyle(lastChild, 'float', true)))) { 17922 dom.add(block, 'br'); 17923 } 17924 } 17925 } 17926 17927 rng = selection.getRng(true); 17928 17929 // Event is blocked by some other handler for example the lists plugin 17930 if (evt.isDefaultPrevented()) { 17931 return; 17932 } 17933 17934 // Delete any selected contents 17935 if (!rng.collapsed) { 17936 editor.execCommand('Delete'); 17937 return; 17938 } 17939 17940 // Setup range items and newBlockName 17941 new RangeUtils(dom).normalize(rng); 17942 container = rng.startContainer; 17943 offset = rng.startOffset; 17944 newBlockName = (settings.force_p_newlines ? 'p' : '') || settings.forced_root_block; 17945 newBlockName = newBlockName ? newBlockName.toUpperCase() : ''; 17946 documentMode = dom.doc.documentMode; 17947 shiftKey = evt.shiftKey; 17948 17949 // Resolve node index 17950 if (container.nodeType == 1 && container.hasChildNodes()) { 17951 isAfterLastNodeInContainer = offset > container.childNodes.length - 1; 17952 17953 container = container.childNodes[Math.min(offset, container.childNodes.length - 1)] || container; 17954 if (isAfterLastNodeInContainer && container.nodeType == 3) { 17955 offset = container.nodeValue.length; 17956 } else { 17957 offset = 0; 17958 } 17959 } 17960 17961 // Get editable root node normaly the body element but sometimes a div or span 17962 editableRoot = getEditableRoot(container); 17963 17964 // If there is no editable root then enter is done inside a contentEditable false element 17965 if (!editableRoot) { 17966 return; 17967 } 17968 17969 undoManager.beforeChange(); 17970 17971 // If editable root isn't block nor the root of the editor 17972 if (!dom.isBlock(editableRoot) && editableRoot != dom.getRoot()) { 17973 if (!newBlockName || shiftKey) { 17974 insertBr(); 17975 } 17976 17977 return; 17978 } 17979 17980 // Wrap the current node and it's sibling in a default block if it's needed. 17981 // for example this <td>text|<b>text2</b></td> will become this <td><p>text|<b>text2</p></b></td> 17982 // This won't happen if root blocks are disabled or the shiftKey is pressed 17983 if ((newBlockName && !shiftKey) || (!newBlockName && shiftKey)) { 17984 container = wrapSelfAndSiblingsInDefaultBlock(container, offset); 17985 } 17986 17987 // Find parent block and setup empty block paddings 17988 parentBlock = dom.getParent(container, dom.isBlock); 17989 containerBlock = parentBlock ? dom.getParent(parentBlock.parentNode, dom.isBlock) : null; 17990 17991 // Setup block names 17992 parentBlockName = parentBlock ? parentBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5 17993 containerBlockName = containerBlock ? containerBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5 17994 17995 // Enter inside block contained within a LI then split or insert before/after LI 17996 if (containerBlockName == 'LI' && !evt.ctrlKey) { 17997 parentBlock = containerBlock; 17998 parentBlockName = containerBlockName; 17999 } 18000 18001 // Handle enter in LI 18002 if (parentBlockName == 'LI') { 18003 if (!newBlockName && shiftKey) { 18004 insertBr(); 18005 return; 18006 } 18007 18008 // Handle enter inside an empty list item 18009 if (dom.isEmpty(parentBlock)) { 18010 handleEmptyListItem(); 18011 return; 18012 } 18013 } 18014 18015 // Don't split PRE tags but insert a BR instead easier when writing code samples etc 18016 if (parentBlockName == 'PRE' && settings.br_in_pre !== false) { 18017 if (!shiftKey) { 18018 insertBr(); 18019 return; 18020 } 18021 } else { 18022 // If no root block is configured then insert a BR by default or if the shiftKey is pressed 18023 if ((!newBlockName && !shiftKey && parentBlockName != 'LI') || (newBlockName && shiftKey)) { 18024 insertBr(); 18025 return; 18026 } 18027 } 18028 18029 // If parent block is root then never insert new blocks 18030 if (newBlockName && parentBlock === editor.getBody()) { 18031 return; 18032 } 18033 18034 // Default block name if it's not configured 18035 newBlockName = newBlockName || 'P'; 18036 18037 // Insert new block before/after the parent block depending on caret location 18038 if (isCaretAtStartOrEndOfBlock()) { 18039 // If the caret is at the end of a header we produce a P tag after it similar to Word unless we are in a hgroup 18040 if (/^(H[1-6]|PRE|FIGURE)$/.test(parentBlockName) && containerBlockName != 'HGROUP') { 18041 newBlock = createNewBlock(newBlockName); 18042 } else { 18043 newBlock = createNewBlock(); 18044 } 18045 18046 // Split the current container block element if enter is pressed inside an empty inner block element 18047 if (settings.end_container_on_empty_block && canSplitBlock(containerBlock) && dom.isEmpty(parentBlock)) { 18048 // Split container block for example a BLOCKQUOTE at the current blockParent location for example a P 18049 newBlock = dom.split(containerBlock, parentBlock); 18050 } else { 18051 dom.insertAfter(newBlock, parentBlock); 18052 } 18053 18054 moveToCaretPosition(newBlock); 18055 } else if (isCaretAtStartOrEndOfBlock(true)) { 18056 // Insert new block before 18057 newBlock = parentBlock.parentNode.insertBefore(createNewBlock(), parentBlock); 18058 renderBlockOnIE(newBlock); 18059 moveToCaretPosition(parentBlock); 18060 } else { 18061 // Extract after fragment and insert it after the current block 18062 tmpRng = rng.cloneRange(); 18063 tmpRng.setEndAfter(parentBlock); 18064 fragment = tmpRng.extractContents(); 18065 trimLeadingLineBreaks(fragment); 18066 newBlock = fragment.firstChild; 18067 dom.insertAfter(fragment, parentBlock); 18068 trimInlineElementsOnLeftSideOfBlock(newBlock); 18069 addBrToBlockIfNeeded(parentBlock); 18070 moveToCaretPosition(newBlock); 18071 } 18072 18073 dom.setAttrib(newBlock, 'id', ''); // Remove ID since it needs to be document unique 18074 18075 // Allow custom handling of new blocks 18076 editor.fire('NewBlock', { newBlock: newBlock }); 18077 18078 undoManager.add(); 18079 } 18080 18081 editor.on('keydown', function(evt) { 18082 if (evt.keyCode == 13) { 18083 if (handleEnterKey(evt) !== false) { 18084 evt.preventDefault(); 18085 } 18086 } 18087 }); 18088 }; 18089 }); 18090 18091 // Included from: js/tinymce/classes/ForceBlocks.js 18092 18093 /** 18094 * ForceBlocks.js 18095 * 18096 * Copyright, Moxiecode Systems AB 18097 * Released under LGPL License. 18098 * 18099 * License: http://www.tinymce.com/license 18100 * Contributing: http://www.tinymce.com/contributing 18101 */ 18102 18103 define("tinymce/ForceBlocks", [], function() { 18104 return function(editor) { 18105 var settings = editor.settings, dom = editor.dom, selection = editor.selection; 18106 var schema = editor.schema, blockElements = schema.getBlockElements(); 18107 18108 function addRootBlocks() { 18109 var node = selection.getStart(), rootNode = editor.getBody(), rng; 18110 var startContainer, startOffset, endContainer, endOffset, rootBlockNode; 18111 var tempNode, offset = -0xFFFFFF, wrapped, restoreSelection; 18112 var tmpRng, rootNodeName, forcedRootBlock; 18113 18114 forcedRootBlock = settings.forced_root_block; 18115 18116 if (!node || node.nodeType !== 1 || !forcedRootBlock) { 18117 return; 18118 } 18119 18120 // Check if node is wrapped in block 18121 while (node && node != rootNode) { 18122 if (blockElements[node.nodeName]) { 18123 return; 18124 } 18125 18126 node = node.parentNode; 18127 } 18128 18129 // Get current selection 18130 rng = selection.getRng(); 18131 if (rng.setStart) { 18132 startContainer = rng.startContainer; 18133 startOffset = rng.startOffset; 18134 endContainer = rng.endContainer; 18135 endOffset = rng.endOffset; 18136 18137 try { 18138 restoreSelection = editor.getDoc().activeElement === rootNode; 18139 } catch (ex) { 18140 // IE throws unspecified error here sometimes 18141 } 18142 } else { 18143 // Force control range into text range 18144 if (rng.item) { 18145 node = rng.item(0); 18146 rng = editor.getDoc().body.createTextRange(); 18147 rng.moveToElementText(node); 18148 } 18149 18150 restoreSelection = rng.parentElement().ownerDocument === editor.getDoc(); 18151 tmpRng = rng.duplicate(); 18152 tmpRng.collapse(true); 18153 startOffset = tmpRng.move('character', offset) * -1; 18154 18155 if (!tmpRng.collapsed) { 18156 tmpRng = rng.duplicate(); 18157 tmpRng.collapse(false); 18158 endOffset = (tmpRng.move('character', offset) * -1) - startOffset; 18159 } 18160 } 18161 18162 // Wrap non block elements and text nodes 18163 node = rootNode.firstChild; 18164 rootNodeName = rootNode.nodeName.toLowerCase(); 18165 while (node) { 18166 // TODO: Break this up, too complex 18167 if (((node.nodeType === 3 || (node.nodeType == 1 && !blockElements[node.nodeName]))) && 18168 schema.isValidChild(rootNodeName, forcedRootBlock.toLowerCase())) { 18169 // Remove empty text nodes 18170 if (node.nodeType === 3 && node.nodeValue.length === 0) { 18171 tempNode = node; 18172 node = node.nextSibling; 18173 dom.remove(tempNode); 18174 continue; 18175 } 18176 18177 if (!rootBlockNode) { 18178 rootBlockNode = dom.create(forcedRootBlock, editor.settings.forced_root_block_attrs); 18179 node.parentNode.insertBefore(rootBlockNode, node); 18180 wrapped = true; 18181 } 18182 18183 tempNode = node; 18184 node = node.nextSibling; 18185 rootBlockNode.appendChild(tempNode); 18186 } else { 18187 rootBlockNode = null; 18188 node = node.nextSibling; 18189 } 18190 } 18191 18192 if (wrapped && restoreSelection) { 18193 if (rng.setStart) { 18194 rng.setStart(startContainer, startOffset); 18195 rng.setEnd(endContainer, endOffset); 18196 selection.setRng(rng); 18197 } else { 18198 // Only select if the previous selection was inside the document to prevent auto focus in quirks mode 18199 try { 18200 rng = editor.getDoc().body.createTextRange(); 18201 rng.moveToElementText(rootNode); 18202 rng.collapse(true); 18203 rng.moveStart('character', startOffset); 18204 18205 if (endOffset > 0) { 18206 rng.moveEnd('character', endOffset); 18207 } 18208 18209 rng.select(); 18210 } catch (ex) { 18211 // Ignore 18212 } 18213 } 18214 18215 editor.nodeChanged(); 18216 } 18217 } 18218 18219 // Force root blocks 18220 if (settings.forced_root_block) { 18221 editor.on('NodeChange', addRootBlocks); 18222 } 18223 }; 18224 }); 18225 18226 // Included from: js/tinymce/classes/EditorCommands.js 18227 18228 /** 18229 * EditorCommands.js 18230 * 18231 * Copyright, Moxiecode Systems AB 18232 * Released under LGPL License. 18233 * 18234 * License: http://www.tinymce.com/license 18235 * Contributing: http://www.tinymce.com/contributing 18236 */ 18237 18238 /** 18239 * This class enables you to add custom editor commands and it contains 18240 * overrides for native browser commands to address various bugs and issues. 18241 * 18242 * @class tinymce.EditorCommands 18243 */ 18244 define("tinymce/EditorCommands", [ 18245 "tinymce/html/Serializer", 18246 "tinymce/Env", 18247 "tinymce/util/Tools" 18248 ], function(Serializer, Env, Tools) { 18249 // Added for compression purposes 18250 var each = Tools.each, extend = Tools.extend; 18251 var map = Tools.map, inArray = Tools.inArray, explode = Tools.explode; 18252 var isGecko = Env.gecko, isIE = Env.ie; 18253 var TRUE = true, FALSE = false; 18254 18255 return function(editor) { 18256 var dom = editor.dom, 18257 selection = editor.selection, 18258 commands = {state: {}, exec: {}, value: {}}, 18259 settings = editor.settings, 18260 formatter = editor.formatter, 18261 bookmark; 18262 18263 /** 18264 * Executes the specified command. 18265 * 18266 * @method execCommand 18267 * @param {String} command Command to execute. 18268 * @param {Boolean} ui Optional user interface state. 18269 * @param {Object} value Optional value for command. 18270 * @return {Boolean} true/false if the command was found or not. 18271 */ 18272 function execCommand(command, ui, value) { 18273 var func; 18274 18275 command = command.toLowerCase(); 18276 if ((func = commands.exec[command])) { 18277 func(command, ui, value); 18278 return TRUE; 18279 } 18280 18281 return FALSE; 18282 } 18283 18284 /** 18285 * Queries the current state for a command for example if the current selection is "bold". 18286 * 18287 * @method queryCommandState 18288 * @param {String} command Command to check the state of. 18289 * @return {Boolean/Number} true/false if the selected contents is bold or not, -1 if it's not found. 18290 */ 18291 function queryCommandState(command) { 18292 var func; 18293 18294 command = command.toLowerCase(); 18295 if ((func = commands.state[command])) { 18296 return func(command); 18297 } 18298 18299 return -1; 18300 } 18301 18302 /** 18303 * Queries the command value for example the current fontsize. 18304 * 18305 * @method queryCommandValue 18306 * @param {String} command Command to check the value of. 18307 * @return {Object} Command value of false if it's not found. 18308 */ 18309 function queryCommandValue(command) { 18310 var func; 18311 18312 command = command.toLowerCase(); 18313 if ((func = commands.value[command])) { 18314 return func(command); 18315 } 18316 18317 return FALSE; 18318 } 18319 18320 /** 18321 * Adds commands to the command collection. 18322 * 18323 * @method addCommands 18324 * @param {Object} command_list Name/value collection with commands to add, the names can also be comma separated. 18325 * @param {String} type Optional type to add, defaults to exec. Can be value or state as well. 18326 */ 18327 function addCommands(command_list, type) { 18328 type = type || 'exec'; 18329 18330 each(command_list, function(callback, command) { 18331 each(command.toLowerCase().split(','), function(command) { 18332 commands[type][command] = callback; 18333 }); 18334 }); 18335 } 18336 18337 // Expose public methods 18338 extend(this, { 18339 execCommand: execCommand, 18340 queryCommandState: queryCommandState, 18341 queryCommandValue: queryCommandValue, 18342 addCommands: addCommands 18343 }); 18344 18345 // Private methods 18346 18347 function execNativeCommand(command, ui, value) { 18348 if (ui === undefined) { 18349 ui = FALSE; 18350 } 18351 18352 if (value === undefined) { 18353 value = null; 18354 } 18355 18356 return editor.getDoc().execCommand(command, ui, value); 18357 } 18358 18359 function isFormatMatch(name) { 18360 return formatter.match(name); 18361 } 18362 18363 function toggleFormat(name, value) { 18364 formatter.toggle(name, value ? {value: value} : undefined); 18365 editor.nodeChanged(); 18366 } 18367 18368 function storeSelection(type) { 18369 bookmark = selection.getBookmark(type); 18370 } 18371 18372 function restoreSelection() { 18373 selection.moveToBookmark(bookmark); 18374 } 18375 18376 // Add execCommand overrides 18377 addCommands({ 18378 // Ignore these, added for compatibility 18379 'mceResetDesignMode,mceBeginUndoLevel': function() {}, 18380 18381 // Add undo manager logic 18382 'mceEndUndoLevel,mceAddUndoLevel': function() { 18383 editor.undoManager.add(); 18384 }, 18385 18386 'Cut,Copy,Paste': function(command) { 18387 var doc = editor.getDoc(), failed; 18388 18389 // Try executing the native command 18390 try { 18391 execNativeCommand(command); 18392 } catch (ex) { 18393 // Command failed 18394 failed = TRUE; 18395 } 18396 18397 // Present alert message about clipboard access not being available 18398 if (failed || !doc.queryCommandSupported(command)) { 18399 var msg = editor.translate( 18400 "Your browser doesn't support direct access to the clipboard. " + 18401 "Please use the Ctrl+X/C/V keyboard shortcuts instead." 18402 ); 18403 18404 if (Env.mac) { 18405 msg = msg.replace(/Ctrl\+/g, '\u2318+'); 18406 } 18407 18408 editor.windowManager.alert(msg); 18409 } 18410 }, 18411 18412 // Override unlink command 18413 unlink: function() { 18414 if (selection.isCollapsed()) { 18415 var elm = selection.getNode(); 18416 if (elm.tagName == 'A') { 18417 editor.dom.remove(elm, true); 18418 } 18419 18420 return; 18421 } 18422 18423 formatter.remove("link"); 18424 }, 18425 18426 // Override justify commands to use the text formatter engine 18427 'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull': function(command) { 18428 var align = command.substring(7); 18429 18430 if (align == 'full') { 18431 align = 'justify'; 18432 } 18433 18434 // Remove all other alignments first 18435 each('left,center,right,justify'.split(','), function(name) { 18436 if (align != name) { 18437 formatter.remove('align' + name); 18438 } 18439 }); 18440 18441 toggleFormat('align' + align); 18442 execCommand('mceRepaint'); 18443 }, 18444 18445 // Override list commands to fix WebKit bug 18446 'InsertUnorderedList,InsertOrderedList': function(command) { 18447 var listElm, listParent; 18448 18449 execNativeCommand(command); 18450 18451 // WebKit produces lists within block elements so we need to split them 18452 // we will replace the native list creation logic to custom logic later on 18453 // TODO: Remove this when the list creation logic is removed 18454 listElm = dom.getParent(selection.getNode(), 'ol,ul'); 18455 if (listElm) { 18456 listParent = listElm.parentNode; 18457 18458 // If list is within a text block then split that block 18459 if (/^(H[1-6]|P|ADDRESS|PRE)$/.test(listParent.nodeName)) { 18460 storeSelection(); 18461 dom.split(listParent, listElm); 18462 restoreSelection(); 18463 } 18464 } 18465 }, 18466 18467 // Override commands to use the text formatter engine 18468 'Bold,Italic,Underline,Strikethrough,Superscript,Subscript': function(command) { 18469 toggleFormat(command); 18470 }, 18471 18472 // Override commands to use the text formatter engine 18473 'ForeColor,HiliteColor,FontName': function(command, ui, value) { 18474 toggleFormat(command, value); 18475 }, 18476 18477 FontSize: function(command, ui, value) { 18478 var fontClasses, fontSizes; 18479 18480 // Convert font size 1-7 to styles 18481 if (value >= 1 && value <= 7) { 18482 fontSizes = explode(settings.font_size_style_values); 18483 fontClasses = explode(settings.font_size_classes); 18484 18485 if (fontClasses) { 18486 value = fontClasses[value - 1] || value; 18487 } else { 18488 value = fontSizes[value - 1] || value; 18489 } 18490 } 18491 18492 toggleFormat(command, value); 18493 }, 18494 18495 RemoveFormat: function(command) { 18496 formatter.remove(command); 18497 }, 18498 18499 mceBlockQuote: function() { 18500 toggleFormat('blockquote'); 18501 }, 18502 18503 FormatBlock: function(command, ui, value) { 18504 return toggleFormat(value || 'p'); 18505 }, 18506 18507 mceCleanup: function() { 18508 var bookmark = selection.getBookmark(); 18509 18510 editor.setContent(editor.getContent({cleanup: TRUE}), {cleanup: TRUE}); 18511 18512 selection.moveToBookmark(bookmark); 18513 }, 18514 18515 mceRemoveNode: function(command, ui, value) { 18516 var node = value || selection.getNode(); 18517 18518 // Make sure that the body node isn't removed 18519 if (node != editor.getBody()) { 18520 storeSelection(); 18521 editor.dom.remove(node, TRUE); 18522 restoreSelection(); 18523 } 18524 }, 18525 18526 mceSelectNodeDepth: function(command, ui, value) { 18527 var counter = 0; 18528 18529 dom.getParent(selection.getNode(), function(node) { 18530 if (node.nodeType == 1 && counter++ == value) { 18531 selection.select(node); 18532 return FALSE; 18533 } 18534 }, editor.getBody()); 18535 }, 18536 18537 mceSelectNode: function(command, ui, value) { 18538 selection.select(value); 18539 }, 18540 18541 mceInsertContent: function(command, ui, value) { 18542 var parser, serializer, parentNode, rootNode, fragment, args; 18543 var marker, rng, node, node2, bookmarkHtml; 18544 18545 function trimOrPaddLeftRight(html) { 18546 var rng, container, offset; 18547 18548 rng = selection.getRng(true); 18549 container = rng.startContainer; 18550 offset = rng.startOffset; 18551 18552 function hasSiblingText(siblingName) { 18553 return container[siblingName] && container[siblingName].nodeType == 3; 18554 } 18555 18556 if (container.nodeType == 3) { 18557 if (offset > 0) { 18558 html = html.replace(/^ /, ' '); 18559 } else if (!hasSiblingText('previousSibling')) { 18560 html = html.replace(/^ /, ' '); 18561 } 18562 18563 if (offset < container.length) { 18564 html = html.replace(/ (<br>|)$/, ' '); 18565 } else if (!hasSiblingText('nextSibling')) { 18566 html = html.replace(/( | )(<br>|)$/, ' '); 18567 } 18568 } 18569 18570 return html; 18571 } 18572 18573 // Check for whitespace before/after value 18574 if (/^ | $/.test(value)) { 18575 value = trimOrPaddLeftRight(value); 18576 } 18577 18578 // Setup parser and serializer 18579 parser = editor.parser; 18580 serializer = new Serializer({}, editor.schema); 18581 bookmarkHtml = '<span id="mce_marker" data-mce-type="bookmark">ÈB;</span>'; 18582 18583 // Run beforeSetContent handlers on the HTML to be inserted 18584 args = {content: value, format: 'html', selection: true}; 18585 editor.fire('BeforeSetContent', args); 18586 value = args.content; 18587 18588 // Add caret at end of contents if it's missing 18589 if (value.indexOf('{$caret}') == -1) { 18590 value += '{$caret}'; 18591 } 18592 18593 // Replace the caret marker with a span bookmark element 18594 value = value.replace(/\{\$caret\}/, bookmarkHtml); 18595 18596 // If selection is at <body>|<p></p> then move it into <body><p>|</p> 18597 rng = selection.getRng(); 18598 var caretElement = rng.startContainer || (rng.parentElement ? rng.parentElement() : null); 18599 var body = editor.getBody(); 18600 if (caretElement === body && selection.isCollapsed()) { 18601 if (dom.isBlock(body.firstChild) && dom.isEmpty(body.firstChild)) { 18602 rng = dom.createRng(); 18603 rng.setStart(body.firstChild, 0); 18604 rng.setEnd(body.firstChild, 0); 18605 selection.setRng(rng); 18606 } 18607 } 18608 18609 // Insert node maker where we will insert the new HTML and get it's parent 18610 if (!selection.isCollapsed()) { 18611 editor.getDoc().execCommand('Delete', false, null); 18612 } 18613 18614 parentNode = selection.getNode(); 18615 18616 // Parse the fragment within the context of the parent node 18617 var parserArgs = {context: parentNode.nodeName.toLowerCase()}; 18618 fragment = parser.parse(value, parserArgs); 18619 18620 // Move the caret to a more suitable location 18621 node = fragment.lastChild; 18622 if (node.attr('id') == 'mce_marker') { 18623 marker = node; 18624 18625 for (node = node.prev; node; node = node.walk(true)) { 18626 if (node.type == 3 || !dom.isBlock(node.name)) { 18627 node.parent.insert(marker, node, node.name === 'br'); 18628 break; 18629 } 18630 } 18631 } 18632 18633 // If parser says valid we can insert the contents into that parent 18634 if (!parserArgs.invalid) { 18635 value = serializer.serialize(fragment); 18636 18637 // Check if parent is empty or only has one BR element then set the innerHTML of that parent 18638 node = parentNode.firstChild; 18639 node2 = parentNode.lastChild; 18640 if (!node || (node === node2 && node.nodeName === 'BR')) { 18641 dom.setHTML(parentNode, value); 18642 } else { 18643 selection.setContent(value); 18644 } 18645 } else { 18646 // If the fragment was invalid within that context then we need 18647 // to parse and process the parent it's inserted into 18648 18649 // Insert bookmark node and get the parent 18650 selection.setContent(bookmarkHtml); 18651 parentNode = selection.getNode(); 18652 rootNode = editor.getBody(); 18653 18654 // Opera will return the document node when selection is in root 18655 if (parentNode.nodeType == 9) { 18656 parentNode = node = rootNode; 18657 } else { 18658 node = parentNode; 18659 } 18660 18661 // Find the ancestor just before the root element 18662 while (node !== rootNode) { 18663 parentNode = node; 18664 node = node.parentNode; 18665 } 18666 18667 // Get the outer/inner HTML depending on if we are in the root and parser and serialize that 18668 value = parentNode == rootNode ? rootNode.innerHTML : dom.getOuterHTML(parentNode); 18669 value = serializer.serialize( 18670 parser.parse( 18671 // Need to replace by using a function since $ in the contents would otherwise be a problem 18672 value.replace(/<span (id="mce_marker"|id=mce_marker).+?<\/span>/i, function() { 18673 return serializer.serialize(fragment); 18674 }) 18675 ) 18676 ); 18677 18678 // Set the inner/outer HTML depending on if we are in the root or not 18679 if (parentNode == rootNode) { 18680 dom.setHTML(rootNode, value); 18681 } else { 18682 dom.setOuterHTML(parentNode, value); 18683 } 18684 } 18685 18686 marker = dom.get('mce_marker'); 18687 selection.scrollIntoView(marker); 18688 18689 // Move selection before marker and remove it 18690 rng = dom.createRng(); 18691 18692 // If previous sibling is a text node set the selection to the end of that node 18693 node = marker.previousSibling; 18694 if (node && node.nodeType == 3) { 18695 rng.setStart(node, node.nodeValue.length); 18696 18697 // TODO: Why can't we normalize on IE 18698 if (!isIE) { 18699 node2 = marker.nextSibling; 18700 if (node2 && node2.nodeType == 3) { 18701 node.appendData(node2.data); 18702 node2.parentNode.removeChild(node2); 18703 } 18704 } 18705 } else { 18706 // If the previous sibling isn't a text node or doesn't exist set the selection before the marker node 18707 rng.setStartBefore(marker); 18708 rng.setEndBefore(marker); 18709 } 18710 18711 // Remove the marker node and set the new range 18712 dom.remove(marker); 18713 selection.setRng(rng); 18714 18715 // Dispatch after event and add any visual elements needed 18716 editor.fire('SetContent', args); 18717 editor.addVisual(); 18718 }, 18719 18720 mceInsertRawHTML: function(command, ui, value) { 18721 selection.setContent('tiny_mce_marker'); 18722 editor.setContent( 18723 editor.getContent().replace(/tiny_mce_marker/g, function() { 18724 return value; 18725 }) 18726 ); 18727 }, 18728 18729 mceToggleFormat: function(command, ui, value) { 18730 toggleFormat(value); 18731 }, 18732 18733 mceSetContent: function(command, ui, value) { 18734 editor.setContent(value); 18735 }, 18736 18737 'Indent,Outdent': function(command) { 18738 var intentValue, indentUnit, value; 18739 18740 // Setup indent level 18741 intentValue = settings.indentation; 18742 indentUnit = /[a-z%]+$/i.exec(intentValue); 18743 intentValue = parseInt(intentValue, 10); 18744 18745 if (!queryCommandState('InsertUnorderedList') && !queryCommandState('InsertOrderedList')) { 18746 // If forced_root_blocks is set to false we don't have a block to indent so lets create a div 18747 if (!settings.forced_root_block && !dom.getParent(selection.getNode(), dom.isBlock)) { 18748 formatter.apply('div'); 18749 } 18750 18751 each(selection.getSelectedBlocks(), function(element) { 18752 if (element.nodeName != "LI") { 18753 var indentStyleName = editor.getParam('indent_use_margin', false) ? 'margin' : 'padding'; 18754 18755 indentStyleName += dom.getStyle(element, 'direction', true) == 'rtl' ? 'Right' : 'Left'; 18756 18757 if (command == 'outdent') { 18758 value = Math.max(0, parseInt(element.style[indentStyleName] || 0, 10) - intentValue); 18759 dom.setStyle(element, indentStyleName, value ? value + indentUnit : ''); 18760 } else { 18761 value = (parseInt(element.style[indentStyleName] || 0, 10) + intentValue) + indentUnit; 18762 dom.setStyle(element, indentStyleName, value); 18763 } 18764 } 18765 }); 18766 } else { 18767 execNativeCommand(command); 18768 } 18769 }, 18770 18771 mceRepaint: function() { 18772 if (isGecko) { 18773 try { 18774 storeSelection(TRUE); 18775 18776 if (selection.getSel()) { 18777 selection.getSel().selectAllChildren(editor.getBody()); 18778 } 18779 18780 selection.collapse(TRUE); 18781 restoreSelection(); 18782 } catch (ex) { 18783 // Ignore 18784 } 18785 } 18786 }, 18787 18788 InsertHorizontalRule: function() { 18789 editor.execCommand('mceInsertContent', false, '<hr />'); 18790 }, 18791 18792 mceToggleVisualAid: function() { 18793 editor.hasVisual = !editor.hasVisual; 18794 editor.addVisual(); 18795 }, 18796 18797 mceReplaceContent: function(command, ui, value) { 18798 editor.execCommand('mceInsertContent', false, value.replace(/\{\$selection\}/g, selection.getContent({format: 'text'}))); 18799 }, 18800 18801 mceInsertLink: function(command, ui, value) { 18802 var anchor; 18803 18804 if (typeof(value) == 'string') { 18805 value = {href: value}; 18806 } 18807 18808 anchor = dom.getParent(selection.getNode(), 'a'); 18809 18810 // Spaces are never valid in URLs and it's a very common mistake for people to make so we fix it here. 18811 value.href = value.href.replace(' ', '%20'); 18812 18813 // Remove existing links if there could be child links or that the href isn't specified 18814 if (!anchor || !value.href) { 18815 formatter.remove('link'); 18816 } 18817 18818 // Apply new link to selection 18819 if (value.href) { 18820 formatter.apply('link', value, anchor); 18821 } 18822 }, 18823 18824 selectAll: function() { 18825 var root = dom.getRoot(), rng; 18826 18827 if (selection.getRng().setStart) { 18828 rng = dom.createRng(); 18829 rng.setStart(root, 0); 18830 rng.setEnd(root, root.childNodes.length); 18831 selection.setRng(rng); 18832 } else { 18833 // IE will render it's own root level block elements and sometimes 18834 // even put font elements in them when the user starts typing. So we need to 18835 // move the selection to a more suitable element from this: 18836 // <body>|<p></p></body> to this: <body><p>|</p></body> 18837 rng = selection.getRng(); 18838 if (!rng.item) { 18839 rng.moveToElementText(root); 18840 rng.select(); 18841 } 18842 } 18843 }, 18844 18845 "delete": function() { 18846 execNativeCommand("Delete"); 18847 18848 // Check if body is empty after the delete call if so then set the contents 18849 // to an empty string and move the caret to any block produced by that operation 18850 // this fixes the issue with root blocks not being properly produced after a delete call on IE 18851 var body = editor.getBody(); 18852 18853 if (dom.isEmpty(body)) { 18854 editor.setContent(''); 18855 18856 if (body.firstChild && dom.isBlock(body.firstChild)) { 18857 editor.selection.setCursorLocation(body.firstChild, 0); 18858 } else { 18859 editor.selection.setCursorLocation(body, 0); 18860 } 18861 } 18862 }, 18863 18864 mceNewDocument: function() { 18865 editor.setContent(''); 18866 } 18867 }); 18868 18869 // Add queryCommandState overrides 18870 addCommands({ 18871 // Override justify commands 18872 'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull': function(command) { 18873 var name = 'align' + command.substring(7); 18874 var nodes = selection.isCollapsed() ? [dom.getParent(selection.getNode(), dom.isBlock)] : selection.getSelectedBlocks(); 18875 var matches = map(nodes, function(node) { 18876 return !!formatter.matchNode(node, name); 18877 }); 18878 return inArray(matches, TRUE) !== -1; 18879 }, 18880 18881 'Bold,Italic,Underline,Strikethrough,Superscript,Subscript': function(command) { 18882 return isFormatMatch(command); 18883 }, 18884 18885 mceBlockQuote: function() { 18886 return isFormatMatch('blockquote'); 18887 }, 18888 18889 Outdent: function() { 18890 var node; 18891 18892 if (settings.inline_styles) { 18893 if ((node = dom.getParent(selection.getStart(), dom.isBlock)) && parseInt(node.style.paddingLeft, 10) > 0) { 18894 return TRUE; 18895 } 18896 18897 if ((node = dom.getParent(selection.getEnd(), dom.isBlock)) && parseInt(node.style.paddingLeft, 10) > 0) { 18898 return TRUE; 18899 } 18900 } 18901 18902 return ( 18903 queryCommandState('InsertUnorderedList') || 18904 queryCommandState('InsertOrderedList') || 18905 (!settings.inline_styles && !!dom.getParent(selection.getNode(), 'BLOCKQUOTE')) 18906 ); 18907 }, 18908 18909 'InsertUnorderedList,InsertOrderedList': function(command) { 18910 var list = dom.getParent(selection.getNode(), 'ul,ol'); 18911 18912 return list && 18913 ( 18914 command === 'insertunorderedlist' && list.tagName === 'UL' || 18915 command === 'insertorderedlist' && list.tagName === 'OL' 18916 ); 18917 } 18918 }, 'state'); 18919 18920 // Add queryCommandValue overrides 18921 addCommands({ 18922 'FontSize,FontName': function(command) { 18923 var value = 0, parent; 18924 18925 if ((parent = dom.getParent(selection.getNode(), 'span'))) { 18926 if (command == 'fontsize') { 18927 value = parent.style.fontSize; 18928 } else { 18929 value = parent.style.fontFamily.replace(/, /g, ',').replace(/[\'\"]/g, '').toLowerCase(); 18930 } 18931 } 18932 18933 return value; 18934 } 18935 }, 'value'); 18936 18937 // Add undo manager logic 18938 addCommands({ 18939 Undo: function() { 18940 editor.undoManager.undo(); 18941 }, 18942 18943 Redo: function() { 18944 editor.undoManager.redo(); 18945 } 18946 }); 18947 }; 18948 }); 18949 18950 // Included from: js/tinymce/classes/util/URI.js 18951 18952 /** 18953 * URI.js 18954 * 18955 * Copyright, Moxiecode Systems AB 18956 * Released under LGPL License. 18957 * 18958 * License: http://www.tinymce.com/license 18959 * Contributing: http://www.tinymce.com/contributing 18960 */ 18961 18962 /** 18963 * This class handles parsing, modification and serialization of URI/URL strings. 18964 * @class tinymce.util.URI 18965 */ 18966 define("tinymce/util/URI", [ 18967 "tinymce/util/Tools" 18968 ], function(Tools) { 18969 var each = Tools.each, trim = Tools.trim, 18970 DEFAULT_PORTS = { 18971 'ftp': 21, 18972 'http': 80, 18973 'https': 443, 18974 'mailto': 25 18975 }; 18976 18977 /** 18978 * Constructs a new URI instance. 18979 * 18980 * @constructor 18981 * @method URI 18982 * @param {String} url URI string to parse. 18983 * @param {Object} settings Optional settings object. 18984 */ 18985 function URI(url, settings) { 18986 var self = this, baseUri, base_url; 18987 18988 // Trim whitespace 18989 url = trim(url); 18990 18991 // Default settings 18992 settings = self.settings = settings || {}; 18993 18994 // Strange app protocol that isn't http/https or local anchor 18995 // For example: mailto,skype,tel etc. 18996 if (/^([\w\-]+):([^\/]{2})/i.test(url) || /^\s*#/.test(url)) { 18997 self.source = url; 18998 return; 18999 } 19000 19001 var isProtocolRelative = url.indexOf('//') === 0; 19002 19003 // Absolute path with no host, fake host and protocol 19004 if (url.indexOf('/') === 0 && !isProtocolRelative) { 19005 url = (settings.base_uri ? settings.base_uri.protocol || 'http' : 'http') + '://mce_host' + url; 19006 } 19007 19008 // Relative path http:// or protocol relative //path 19009 if (!/^[\w\-]*:?\/\//.test(url)) { 19010 base_url = settings.base_uri ? settings.base_uri.path : new URI(location.href).directory; 19011 if (settings.base_uri.protocol === "") { 19012 url = '//mce_host' + self.toAbsPath(base_url, url); 19013 } else { 19014 url = ((settings.base_uri && settings.base_uri.protocol) || 'http') + '://mce_host' + self.toAbsPath(base_url, url); 19015 } 19016 } 19017 19018 // Parse URL (Credits goes to Steave, http://blog.stevenlevithan.com/archives/parseuri) 19019 url = url.replace(/@@/g, '(mce_at)'); // Zope 3 workaround, they use @@something 19020 19021 /*jshint maxlen: 255 */ 19022 /*eslint max-len: 0 */ 19023 url = /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@\/]*):?([^:@\/]*))?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/.exec(url); 19024 19025 each(["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"], function(v, i) { 19026 var part = url[i]; 19027 19028 // Zope 3 workaround, they use @@something 19029 if (part) { 19030 part = part.replace(/\(mce_at\)/g, '@@'); 19031 } 19032 19033 self[v] = part; 19034 }); 19035 19036 baseUri = settings.base_uri; 19037 if (baseUri) { 19038 if (!self.protocol) { 19039 self.protocol = baseUri.protocol; 19040 } 19041 19042 if (!self.userInfo) { 19043 self.userInfo = baseUri.userInfo; 19044 } 19045 19046 if (!self.port && self.host === 'mce_host') { 19047 self.port = baseUri.port; 19048 } 19049 19050 if (!self.host || self.host === 'mce_host') { 19051 self.host = baseUri.host; 19052 } 19053 19054 self.source = ''; 19055 } 19056 19057 if (isProtocolRelative) { 19058 self.protocol = ''; 19059 } 19060 19061 //t.path = t.path || '/'; 19062 } 19063 19064 URI.prototype = { 19065 /** 19066 * Sets the internal path part of the URI. 19067 * 19068 * @method setPath 19069 * @param {string} path Path string to set. 19070 */ 19071 setPath: function(path) { 19072 var self = this; 19073 19074 path = /^(.*?)\/?(\w+)?$/.exec(path); 19075 19076 // Update path parts 19077 self.path = path[0]; 19078 self.directory = path[1]; 19079 self.file = path[2]; 19080 19081 // Rebuild source 19082 self.source = ''; 19083 self.getURI(); 19084 }, 19085 19086 /** 19087 * Converts the specified URI into a relative URI based on the current URI instance location. 19088 * 19089 * @method toRelative 19090 * @param {String} uri URI to convert into a relative path/URI. 19091 * @return {String} Relative URI from the point specified in the current URI instance. 19092 * @example 19093 * // Converts an absolute URL to an relative URL url will be somedir/somefile.htm 19094 * var url = new tinymce.util.URI('http://www.site.com/dir/').toRelative('http://www.site.com/dir/somedir/somefile.htm'); 19095 */ 19096 toRelative: function(uri) { 19097 var self = this, output; 19098 19099 if (uri === "./") { 19100 return uri; 19101 } 19102 19103 uri = new URI(uri, {base_uri: self}); 19104 19105 // Not on same domain/port or protocol 19106 if ((uri.host != 'mce_host' && self.host != uri.host && uri.host) || self.port != uri.port || 19107 (self.protocol != uri.protocol && uri.protocol !== "")) { 19108 return uri.getURI(); 19109 } 19110 19111 var tu = self.getURI(), uu = uri.getURI(); 19112 19113 // Allow usage of the base_uri when relative_urls = true 19114 if (tu == uu || (tu.charAt(tu.length - 1) == "/" && tu.substr(0, tu.length - 1) == uu)) { 19115 return tu; 19116 } 19117 19118 output = self.toRelPath(self.path, uri.path); 19119 19120 // Add query 19121 if (uri.query) { 19122 output += '?' + uri.query; 19123 } 19124 19125 // Add anchor 19126 if (uri.anchor) { 19127 output += '#' + uri.anchor; 19128 } 19129 19130 return output; 19131 }, 19132 19133 /** 19134 * Converts the specified URI into a absolute URI based on the current URI instance location. 19135 * 19136 * @method toAbsolute 19137 * @param {String} uri URI to convert into a relative path/URI. 19138 * @param {Boolean} noHost No host and protocol prefix. 19139 * @return {String} Absolute URI from the point specified in the current URI instance. 19140 * @example 19141 * // Converts an relative URL to an absolute URL url will be http://www.site.com/dir/somedir/somefile.htm 19142 * var url = new tinymce.util.URI('http://www.site.com/dir/').toAbsolute('somedir/somefile.htm'); 19143 */ 19144 toAbsolute: function(uri, noHost) { 19145 uri = new URI(uri, {base_uri: this}); 19146 19147 return uri.getURI(noHost && this.isSameOrigin(uri)); 19148 }, 19149 19150 /** 19151 * Determine whether the given URI has the same origin as this URI. Based on RFC-6454. 19152 * Supports default ports for protocols listed in DEFAULT_PORTS. Unsupported protocols will fail safe: they 19153 * won't match, if the port specifications differ. 19154 * 19155 * @method isSameOrigin 19156 * @param {tinymce.util.URI} uri Uri instance to compare. 19157 * @returns {Boolean} True if the origins are the same. 19158 */ 19159 isSameOrigin: function(uri) { 19160 if (this.host == uri.host && this.protocol == uri.protocol){ 19161 if (this.port == uri.port) { 19162 return true; 19163 } 19164 19165 var defaultPort = DEFAULT_PORTS[this.protocol]; 19166 if (defaultPort && ((this.port || defaultPort) == (uri.port || defaultPort))) { 19167 return true; 19168 } 19169 } 19170 19171 return false; 19172 }, 19173 19174 /** 19175 * Converts a absolute path into a relative path. 19176 * 19177 * @method toRelPath 19178 * @param {String} base Base point to convert the path from. 19179 * @param {String} path Absolute path to convert into a relative path. 19180 */ 19181 toRelPath: function(base, path) { 19182 var items, breakPoint = 0, out = '', i, l; 19183 19184 // Split the paths 19185 base = base.substring(0, base.lastIndexOf('/')); 19186 base = base.split('/'); 19187 items = path.split('/'); 19188 19189 if (base.length >= items.length) { 19190 for (i = 0, l = base.length; i < l; i++) { 19191 if (i >= items.length || base[i] != items[i]) { 19192 breakPoint = i + 1; 19193 break; 19194 } 19195 } 19196 } 19197 19198 if (base.length < items.length) { 19199 for (i = 0, l = items.length; i < l; i++) { 19200 if (i >= base.length || base[i] != items[i]) { 19201 breakPoint = i + 1; 19202 break; 19203 } 19204 } 19205 } 19206 19207 if (breakPoint === 1) { 19208 return path; 19209 } 19210 19211 for (i = 0, l = base.length - (breakPoint - 1); i < l; i++) { 19212 out += "../"; 19213 } 19214 19215 for (i = breakPoint - 1, l = items.length; i < l; i++) { 19216 if (i != breakPoint - 1) { 19217 out += "/" + items[i]; 19218 } else { 19219 out += items[i]; 19220 } 19221 } 19222 19223 return out; 19224 }, 19225 19226 /** 19227 * Converts a relative path into a absolute path. 19228 * 19229 * @method toAbsPath 19230 * @param {String} base Base point to convert the path from. 19231 * @param {String} path Relative path to convert into an absolute path. 19232 */ 19233 toAbsPath: function(base, path) { 19234 var i, nb = 0, o = [], tr, outPath; 19235 19236 // Split paths 19237 tr = /\/$/.test(path) ? '/' : ''; 19238 base = base.split('/'); 19239 path = path.split('/'); 19240 19241 // Remove empty chunks 19242 each(base, function(k) { 19243 if (k) { 19244 o.push(k); 19245 } 19246 }); 19247 19248 base = o; 19249 19250 // Merge relURLParts chunks 19251 for (i = path.length - 1, o = []; i >= 0; i--) { 19252 // Ignore empty or . 19253 if (path[i].length === 0 || path[i] === ".") { 19254 continue; 19255 } 19256 19257 // Is parent 19258 if (path[i] === '..') { 19259 nb++; 19260 continue; 19261 } 19262 19263 // Move up 19264 if (nb > 0) { 19265 nb--; 19266 continue; 19267 } 19268 19269 o.push(path[i]); 19270 } 19271 19272 i = base.length - nb; 19273 19274 // If /a/b/c or / 19275 if (i <= 0) { 19276 outPath = o.reverse().join('/'); 19277 } else { 19278 outPath = base.slice(0, i).join('/') + '/' + o.reverse().join('/'); 19279 } 19280 19281 // Add front / if it's needed 19282 if (outPath.indexOf('/') !== 0) { 19283 outPath = '/' + outPath; 19284 } 19285 19286 // Add traling / if it's needed 19287 if (tr && outPath.lastIndexOf('/') !== outPath.length - 1) { 19288 outPath += tr; 19289 } 19290 19291 return outPath; 19292 }, 19293 19294 /** 19295 * Returns the full URI of the internal structure. 19296 * 19297 * @method getURI 19298 * @param {Boolean} noProtoHost Optional no host and protocol part. Defaults to false. 19299 */ 19300 getURI: function(noProtoHost) { 19301 var s, self = this; 19302 19303 // Rebuild source 19304 if (!self.source || noProtoHost) { 19305 s = ''; 19306 19307 if (!noProtoHost) { 19308 if (self.protocol) { 19309 s += self.protocol + '://'; 19310 } else { 19311 s += '//'; 19312 } 19313 19314 if (self.userInfo) { 19315 s += self.userInfo + '@'; 19316 } 19317 19318 if (self.host) { 19319 s += self.host; 19320 } 19321 19322 if (self.port) { 19323 s += ':' + self.port; 19324 } 19325 } 19326 19327 if (self.path) { 19328 s += self.path; 19329 } 19330 19331 if (self.query) { 19332 s += '?' + self.query; 19333 } 19334 19335 if (self.anchor) { 19336 s += '#' + self.anchor; 19337 } 19338 19339 self.source = s; 19340 } 19341 19342 return self.source; 19343 } 19344 }; 19345 19346 return URI; 19347 }); 19348 19349 // Included from: js/tinymce/classes/util/Class.js 19350 19351 /** 19352 * Class.js 19353 * 19354 * Copyright 2003-2012, Moxiecode Systems AB, All rights reserved. 19355 */ 19356 19357 /** 19358 * This utilitiy class is used for easier inheritage. 19359 * 19360 * Features: 19361 * * Exposed super functions: this._super(); 19362 * * Mixins 19363 * * Dummy functions 19364 * * Property functions: var value = object.value(); and object.value(newValue); 19365 * * Static functions 19366 * * Defaults settings 19367 */ 19368 define("tinymce/util/Class", [ 19369 "tinymce/util/Tools" 19370 ], function(Tools) { 19371 var each = Tools.each, extend = Tools.extend; 19372 19373 var extendClass, initializing; 19374 19375 function Class() { 19376 } 19377 19378 // Provides classical inheritance, based on code made by John Resig 19379 Class.extend = extendClass = function(prop) { 19380 var self = this, _super = self.prototype, prototype, name, member; 19381 19382 // The dummy class constructor 19383 function Class() { 19384 var i, mixins, mixin, self = this; 19385 19386 // All construction is actually done in the init method 19387 if (!initializing) { 19388 // Run class constuctor 19389 if (self.init) { 19390 self.init.apply(self, arguments); 19391 } 19392 19393 // Run mixin constructors 19394 mixins = self.Mixins; 19395 if (mixins) { 19396 i = mixins.length; 19397 while (i--) { 19398 mixin = mixins[i]; 19399 if (mixin.init) { 19400 mixin.init.apply(self, arguments); 19401 } 19402 } 19403 } 19404 } 19405 } 19406 19407 // Dummy function, needs to be extended in order to provide functionality 19408 function dummy() { 19409 return this; 19410 } 19411 19412 // Creates a overloaded method for the class 19413 // this enables you to use this._super(); to call the super function 19414 function createMethod(name, fn) { 19415 return function(){ 19416 var self = this, tmp = self._super, ret; 19417 19418 self._super = _super[name]; 19419 ret = fn.apply(self, arguments); 19420 self._super = tmp; 19421 19422 return ret; 19423 }; 19424 } 19425 19426 // Instantiate a base class (but only create the instance, 19427 // don't run the init constructor) 19428 initializing = true; 19429 19430 /*eslint new-cap:0 */ 19431 prototype = new self(); 19432 initializing = false; 19433 19434 // Add mixins 19435 if (prop.Mixins) { 19436 each(prop.Mixins, function(mixin) { 19437 mixin = mixin; 19438 19439 for (var name in mixin) { 19440 if (name !== "init") { 19441 prop[name] = mixin[name]; 19442 } 19443 } 19444 }); 19445 19446 if (_super.Mixins) { 19447 prop.Mixins = _super.Mixins.concat(prop.Mixins); 19448 } 19449 } 19450 19451 // Generate dummy methods 19452 if (prop.Methods) { 19453 each(prop.Methods.split(','), function(name) { 19454 prop[name] = dummy; 19455 }); 19456 } 19457 19458 // Generate property methods 19459 if (prop.Properties) { 19460 each(prop.Properties.split(','), function(name) { 19461 var fieldName = '_' + name; 19462 19463 prop[name] = function(value) { 19464 var self = this, undef; 19465 19466 // Set value 19467 if (value !== undef) { 19468 self[fieldName] = value; 19469 19470 return self; 19471 } 19472 19473 // Get value 19474 return self[fieldName]; 19475 }; 19476 }); 19477 } 19478 19479 // Static functions 19480 if (prop.Statics) { 19481 each(prop.Statics, function(func, name) { 19482 Class[name] = func; 19483 }); 19484 } 19485 19486 // Default settings 19487 if (prop.Defaults && _super.Defaults) { 19488 prop.Defaults = extend({}, _super.Defaults, prop.Defaults); 19489 } 19490 19491 // Copy the properties over onto the new prototype 19492 for (name in prop) { 19493 member = prop[name]; 19494 19495 if (typeof member == "function" && _super[name]) { 19496 prototype[name] = createMethod(name, member); 19497 } else { 19498 prototype[name] = member; 19499 } 19500 } 19501 19502 // Populate our constructed prototype object 19503 Class.prototype = prototype; 19504 19505 // Enforce the constructor to be what we expect 19506 Class.constructor = Class; 19507 19508 // And make this class extendible 19509 Class.extend = extendClass; 19510 19511 return Class; 19512 }; 19513 19514 return Class; 19515 }); 19516 19517 // Included from: js/tinymce/classes/util/EventDispatcher.js 19518 19519 /** 19520 * EventDispatcher.js 19521 * 19522 * Copyright, Moxiecode Systems AB 19523 * Released under LGPL License. 19524 * 19525 * License: http://www.tinymce.com/license 19526 * Contributing: http://www.tinymce.com/contributing 19527 */ 19528 19529 /** 19530 * This class lets you add/remove and fire events by name on the specified scope. This makes 19531 * it easy to add event listener logic to any class. 19532 * 19533 * @class tinymce.util.EventDispatcher 19534 * @example 19535 * var eventDispatcher = new EventDispatcher(); 19536 * 19537 * eventDispatcher.on('click', function() {console.log('data');}); 19538 * eventDispatcher.fire('click', {data: 123}); 19539 */ 19540 define("tinymce/util/EventDispatcher", [ 19541 "tinymce/util/Tools" 19542 ], function(Tools) { 19543 var nativeEvents = Tools.makeMap( 19544 "focus blur focusin focusout click dblclick mousedown mouseup mousemove mouseover beforepaste paste cut copy selectionchange " + 19545 "mouseout mouseenter mouseleave wheel keydown keypress keyup input contextmenu dragstart dragend dragover " + 19546 "draggesture dragdrop drop drag submit", 19547 ' ' 19548 ); 19549 19550 function Dispatcher(settings) { 19551 var self = this, scope, bindings = {}, toggleEvent; 19552 19553 function returnFalse() { 19554 return false; 19555 } 19556 19557 function returnTrue() { 19558 return true; 19559 } 19560 19561 settings = settings || {}; 19562 scope = settings.scope || self; 19563 toggleEvent = settings.toggleEvent || returnFalse; 19564 19565 /** 19566 * Fires the specified event by name. 19567 * 19568 * @method fire 19569 * @param {String} name Name of the event to fire. 19570 * @param {Object?} args Event arguments. 19571 * @return {Object} Event args instance passed in. 19572 * @example 19573 * instance.fire('event', {...}); 19574 */ 19575 function fire(name, args) { 19576 var handlers, i, l, callback; 19577 19578 name = name.toLowerCase(); 19579 args = args || {}; 19580 args.type = name; 19581 19582 // Setup target is there isn't one 19583 if (!args.target) { 19584 args.target = scope; 19585 } 19586 19587 // Add event delegation methods if they are missing 19588 if (!args.preventDefault) { 19589 // Add preventDefault method 19590 args.preventDefault = function() { 19591 args.isDefaultPrevented = returnTrue; 19592 }; 19593 19594 // Add stopPropagation 19595 args.stopPropagation = function() { 19596 args.isPropagationStopped = returnTrue; 19597 }; 19598 19599 // Add stopImmediatePropagation 19600 args.stopImmediatePropagation = function() { 19601 args.isImmediatePropagationStopped = returnTrue; 19602 }; 19603 19604 // Add event delegation states 19605 args.isDefaultPrevented = returnFalse; 19606 args.isPropagationStopped = returnFalse; 19607 args.isImmediatePropagationStopped = returnFalse; 19608 } 19609 19610 if (settings.beforeFire) { 19611 settings.beforeFire(args); 19612 } 19613 19614 handlers = bindings[name]; 19615 if (handlers) { 19616 for (i = 0, l = handlers.length; i < l; i++) { 19617 handlers[i] = callback = handlers[i]; 19618 19619 // Stop immediate propagation if needed 19620 if (args.isImmediatePropagationStopped()) { 19621 args.stopPropagation(); 19622 return args; 19623 } 19624 19625 // If callback returns false then prevent default and stop all propagation 19626 if (callback.call(scope, args) === false) { 19627 args.preventDefault(); 19628 return args; 19629 } 19630 } 19631 } 19632 19633 return args; 19634 } 19635 19636 /** 19637 * Binds an event listener to a specific event by name. 19638 * 19639 * @method on 19640 * @param {String} name Event name or space separated list of events to bind. 19641 * @param {callback} callback Callback to be executed when the event occurs. 19642 * @param {Boolean} first Optional flag if the event should be prepended. Use this with care. 19643 * @return {Object} Current class instance. 19644 * @example 19645 * instance.on('event', function(e) { 19646 * // Callback logic 19647 * }); 19648 */ 19649 function on(name, callback, prepend) { 19650 var handlers, names, i; 19651 19652 if (callback === false) { 19653 callback = returnFalse; 19654 } 19655 19656 if (callback) { 19657 names = name.toLowerCase().split(' '); 19658 i = names.length; 19659 while (i--) { 19660 name = names[i]; 19661 handlers = bindings[name]; 19662 if (!handlers) { 19663 handlers = bindings[name] = []; 19664 toggleEvent(name, true); 19665 } 19666 19667 if (prepend) { 19668 handlers.unshift(callback); 19669 } else { 19670 handlers.push(callback); 19671 } 19672 } 19673 } 19674 19675 return self; 19676 } 19677 19678 /** 19679 * Unbinds an event listener to a specific event by name. 19680 * 19681 * @method off 19682 * @param {String?} name Name of the event to unbind. 19683 * @param {callback?} callback Callback to unbind. 19684 * @return {Object} Current class instance. 19685 * @example 19686 * // Unbind specific callback 19687 * instance.off('event', handler); 19688 * 19689 * // Unbind all listeners by name 19690 * instance.off('event'); 19691 * 19692 * // Unbind all events 19693 * instance.off(); 19694 */ 19695 function off(name, callback) { 19696 var i, handlers, bindingName, names, hi; 19697 19698 if (name) { 19699 names = name.toLowerCase().split(' '); 19700 i = names.length; 19701 while (i--) { 19702 name = names[i]; 19703 handlers = bindings[name]; 19704 19705 // Unbind all handlers 19706 if (!name) { 19707 for (bindingName in bindings) { 19708 toggleEvent(bindingName, false); 19709 delete bindings[bindingName]; 19710 } 19711 19712 return self; 19713 } 19714 19715 if (handlers) { 19716 // Unbind all by name 19717 if (!callback) { 19718 handlers.length = 0; 19719 } else { 19720 // Unbind specific ones 19721 hi = handlers.length; 19722 while (hi--) { 19723 if (handlers[hi] === callback) { 19724 handlers.splice(hi, 1); 19725 } 19726 } 19727 } 19728 19729 if (!handlers.length) { 19730 toggleEvent(name, false); 19731 delete bindings[name]; 19732 } 19733 } 19734 } 19735 } else { 19736 for (name in bindings) { 19737 toggleEvent(name, false); 19738 } 19739 19740 bindings = {}; 19741 } 19742 19743 return self; 19744 } 19745 19746 /** 19747 * Returns true/false if the dispatcher has a event of the specified name. 19748 * 19749 * @method has 19750 * @param {String} name Name of the event to check for. 19751 * @return {Boolean} true/false if the event exists or not. 19752 */ 19753 function has(name) { 19754 name = name.toLowerCase(); 19755 return !(!bindings[name] || bindings[name].length === 0); 19756 } 19757 19758 // Expose 19759 self.fire = fire; 19760 self.on = on; 19761 self.off = off; 19762 self.has = has; 19763 } 19764 19765 /** 19766 * Returns true/false if the specified event name is a native browser event or not. 19767 * 19768 * @method isNative 19769 * @param {String} name Name to check if it's native. 19770 * @return {Boolean} true/false if the event is native or not. 19771 * @static 19772 */ 19773 Dispatcher.isNative = function(name) { 19774 return !!nativeEvents[name.toLowerCase()]; 19775 }; 19776 19777 return Dispatcher; 19778 }); 19779 19780 // Included from: js/tinymce/classes/ui/Selector.js 19781 19782 /** 19783 * Selector.js 19784 * 19785 * Copyright, Moxiecode Systems AB 19786 * Released under LGPL License. 19787 * 19788 * License: http://www.tinymce.com/license 19789 * Contributing: http://www.tinymce.com/contributing 19790 */ 19791 19792 /*eslint no-nested-ternary:0 */ 19793 19794 /** 19795 * Selector engine, enables you to select controls by using CSS like expressions. 19796 * We currently only support basic CSS expressions to reduce the size of the core 19797 * and the ones we support should be enough for most cases. 19798 * 19799 * @example 19800 * Supported expressions: 19801 * element 19802 * element#name 19803 * element.class 19804 * element[attr] 19805 * element[attr*=value] 19806 * element[attr~=value] 19807 * element[attr!=value] 19808 * element[attr^=value] 19809 * element[attr$=value] 19810 * element:<state> 19811 * element:not(<expression>) 19812 * element:first 19813 * element:last 19814 * element:odd 19815 * element:even 19816 * element element 19817 * element > element 19818 * 19819 * @class tinymce.ui.Selector 19820 */ 19821 define("tinymce/ui/Selector", [ 19822 "tinymce/util/Class" 19823 ], function(Class) { 19824 "use strict"; 19825 19826 /** 19827 * Produces an array with a unique set of objects. It will not compare the values 19828 * but the references of the objects. 19829 * 19830 * @private 19831 * @method unqiue 19832 * @param {Array} array Array to make into an array with unique items. 19833 * @return {Array} Array with unique items. 19834 */ 19835 function unique(array) { 19836 var uniqueItems = [], i = array.length, item; 19837 19838 while (i--) { 19839 item = array[i]; 19840 19841 if (!item.__checked) { 19842 uniqueItems.push(item); 19843 item.__checked = 1; 19844 } 19845 } 19846 19847 i = uniqueItems.length; 19848 while (i--) { 19849 delete uniqueItems[i].__checked; 19850 } 19851 19852 return uniqueItems; 19853 } 19854 19855 var expression = /^([\w\\*]+)?(?:#([\w\\]+))?(?:\.([\w\\\.]+))?(?:\[\@?([\w\\]+)([\^\$\*!~]?=)([\w\\]+)\])?(?:\:(.+))?/i; 19856 19857 /*jshint maxlen:255 */ 19858 /*eslint max-len:0 */ 19859 var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, 19860 whiteSpace = /^\s*|\s*$/g, 19861 Collection; 19862 19863 var Selector = Class.extend({ 19864 /** 19865 * Constructs a new Selector instance. 19866 * 19867 * @constructor 19868 * @method init 19869 * @param {String} selector CSS like selector expression. 19870 */ 19871 init: function(selector) { 19872 var match = this.match; 19873 19874 function compileNameFilter(name) { 19875 if (name) { 19876 name = name.toLowerCase(); 19877 19878 return function(item) { 19879 return name === '*' || item.type === name; 19880 }; 19881 } 19882 } 19883 19884 function compileIdFilter(id) { 19885 if (id) { 19886 return function(item) { 19887 return item._name === id; 19888 }; 19889 } 19890 } 19891 19892 function compileClassesFilter(classes) { 19893 if (classes) { 19894 classes = classes.split('.'); 19895 19896 return function(item) { 19897 var i = classes.length; 19898 19899 while (i--) { 19900 if (!item.hasClass(classes[i])) { 19901 return false; 19902 } 19903 } 19904 19905 return true; 19906 }; 19907 } 19908 } 19909 19910 function compileAttrFilter(name, cmp, check) { 19911 if (name) { 19912 return function(item) { 19913 var value = item[name] ? item[name]() : ''; 19914 19915 return !cmp ? !!check : 19916 cmp === "=" ? value === check : 19917 cmp === "*=" ? value.indexOf(check) >= 0 : 19918 cmp === "~=" ? (" " + value + " ").indexOf(" " + check + " ") >= 0 : 19919 cmp === "!=" ? value != check : 19920 cmp === "^=" ? value.indexOf(check) === 0 : 19921 cmp === "$=" ? value.substr(value.length - check.length) === check : 19922 false; 19923 }; 19924 } 19925 } 19926 19927 function compilePsuedoFilter(name) { 19928 var notSelectors; 19929 19930 if (name) { 19931 name = /(?:not\((.+)\))|(.+)/i.exec(name); 19932 19933 if (!name[1]) { 19934 name = name[2]; 19935 19936 return function(item, index, length) { 19937 return name === 'first' ? index === 0 : 19938 name === 'last' ? index === length - 1 : 19939 name === 'even' ? index % 2 === 0 : 19940 name === 'odd' ? index % 2 === 1 : 19941 item[name] ? item[name]() : 19942 false; 19943 }; 19944 } else { 19945 // Compile not expression 19946 notSelectors = parseChunks(name[1], []); 19947 19948 return function(item) { 19949 return !match(item, notSelectors); 19950 }; 19951 } 19952 } 19953 } 19954 19955 function compile(selector, filters, direct) { 19956 var parts; 19957 19958 function add(filter) { 19959 if (filter) { 19960 filters.push(filter); 19961 } 19962 } 19963 19964 // Parse expression into parts 19965 parts = expression.exec(selector.replace(whiteSpace, '')); 19966 19967 add(compileNameFilter(parts[1])); 19968 add(compileIdFilter(parts[2])); 19969 add(compileClassesFilter(parts[3])); 19970 add(compileAttrFilter(parts[4], parts[5], parts[6])); 19971 add(compilePsuedoFilter(parts[7])); 19972 19973 // Mark the filter with psuedo for performance 19974 filters.psuedo = !!parts[7]; 19975 filters.direct = direct; 19976 19977 return filters; 19978 } 19979 19980 // Parser logic based on Sizzle by John Resig 19981 function parseChunks(selector, selectors) { 19982 var parts = [], extra, matches, i; 19983 19984 do { 19985 chunker.exec(""); 19986 matches = chunker.exec(selector); 19987 19988 if (matches) { 19989 selector = matches[3]; 19990 parts.push(matches[1]); 19991 19992 if (matches[2]) { 19993 extra = matches[3]; 19994 break; 19995 } 19996 } 19997 } while (matches); 19998 19999 if (extra) { 20000 parseChunks(extra, selectors); 20001 } 20002 20003 selector = []; 20004 for (i = 0; i < parts.length; i++) { 20005 if (parts[i] != '>') { 20006 selector.push(compile(parts[i], [], parts[i - 1] === '>')); 20007 } 20008 } 20009 20010 selectors.push(selector); 20011 20012 return selectors; 20013 } 20014 20015 this._selectors = parseChunks(selector, []); 20016 }, 20017 20018 /** 20019 * Returns true/false if the selector matches the specified control. 20020 * 20021 * @method match 20022 * @param {tinymce.ui.Control} control Control to match agains the selector. 20023 * @param {Array} selectors Optional array of selectors, mostly used internally. 20024 * @return {Boolean} true/false state if the control matches or not. 20025 */ 20026 match: function(control, selectors) { 20027 var i, l, si, sl, selector, fi, fl, filters, index, length, siblings, count, item; 20028 20029 selectors = selectors || this._selectors; 20030 for (i = 0, l = selectors.length; i < l; i++) { 20031 selector = selectors[i]; 20032 sl = selector.length; 20033 item = control; 20034 count = 0; 20035 20036 for (si = sl - 1; si >= 0; si--) { 20037 filters = selector[si]; 20038 20039 while (item) { 20040 // Find the index and length since a psuedo filter like :first needs it 20041 if (filters.psuedo) { 20042 siblings = item.parent().items(); 20043 index = length = siblings.length; 20044 while (index--) { 20045 if (siblings[index] === item) { 20046 break; 20047 } 20048 } 20049 } 20050 20051 for (fi = 0, fl = filters.length; fi < fl; fi++) { 20052 if (!filters[fi](item, index, length)) { 20053 fi = fl + 1; 20054 break; 20055 } 20056 } 20057 20058 if (fi === fl) { 20059 count++; 20060 break; 20061 } else { 20062 // If it didn't match the right most expression then 20063 // break since it's no point looking at the parents 20064 if (si === sl - 1) { 20065 break; 20066 } 20067 } 20068 20069 item = item.parent(); 20070 } 20071 } 20072 20073 // If we found all selectors then return true otherwise continue looking 20074 if (count === sl) { 20075 return true; 20076 } 20077 } 20078 20079 return false; 20080 }, 20081 20082 /** 20083 * Returns a tinymce.ui.Collection with matches of the specified selector inside the specified container. 20084 * 20085 * @method find 20086 * @param {tinymce.ui.Control} container Container to look for items in. 20087 * @return {tinymce.ui.Collection} Collection with matched elements. 20088 */ 20089 find: function(container) { 20090 var matches = [], i, l, selectors = this._selectors; 20091 20092 function collect(items, selector, index) { 20093 var i, l, fi, fl, item, filters = selector[index]; 20094 20095 for (i = 0, l = items.length; i < l; i++) { 20096 item = items[i]; 20097 20098 // Run each filter agains the item 20099 for (fi = 0, fl = filters.length; fi < fl; fi++) { 20100 if (!filters[fi](item, i, l)) { 20101 fi = fl + 1; 20102 break; 20103 } 20104 } 20105 20106 // All filters matched the item 20107 if (fi === fl) { 20108 // Matched item is on the last expression like: panel toolbar [button] 20109 if (index == selector.length - 1) { 20110 matches.push(item); 20111 } else { 20112 // Collect next expression type 20113 if (item.items) { 20114 collect(item.items(), selector, index + 1); 20115 } 20116 } 20117 } else if (filters.direct) { 20118 return; 20119 } 20120 20121 // Collect child items 20122 if (item.items) { 20123 collect(item.items(), selector, index); 20124 } 20125 } 20126 } 20127 20128 if (container.items) { 20129 for (i = 0, l = selectors.length; i < l; i++) { 20130 collect(container.items(), selectors[i], 0); 20131 } 20132 20133 // Unique the matches if needed 20134 if (l > 1) { 20135 matches = unique(matches); 20136 } 20137 } 20138 20139 // Fix for circular reference 20140 if (!Collection) { 20141 // TODO: Fix me! 20142 Collection = Selector.Collection; 20143 } 20144 20145 return new Collection(matches); 20146 } 20147 }); 20148 20149 return Selector; 20150 }); 20151 20152 // Included from: js/tinymce/classes/ui/Collection.js 20153 20154 /** 20155 * Collection.js 20156 * 20157 * Copyright, Moxiecode Systems AB 20158 * Released under LGPL License. 20159 * 20160 * License: http://www.tinymce.com/license 20161 * Contributing: http://www.tinymce.com/contributing 20162 */ 20163 20164 /** 20165 * Control collection, this class contains control instances and it enables you to 20166 * perform actions on all the contained items. This is very similar to how jQuery works. 20167 * 20168 * @example 20169 * someCollection.show().disabled(true); 20170 * 20171 * @class tinymce.ui.Collection 20172 */ 20173 define("tinymce/ui/Collection", [ 20174 "tinymce/util/Tools", 20175 "tinymce/ui/Selector", 20176 "tinymce/util/Class" 20177 ], function(Tools, Selector, Class) { 20178 "use strict"; 20179 20180 var Collection, proto, push = Array.prototype.push, slice = Array.prototype.slice; 20181 20182 proto = { 20183 /** 20184 * Current number of contained control instances. 20185 * 20186 * @field length 20187 * @type Number 20188 */ 20189 length: 0, 20190 20191 /** 20192 * Constructor for the collection. 20193 * 20194 * @constructor 20195 * @method init 20196 * @param {Array} items Optional array with items to add. 20197 */ 20198 init: function(items) { 20199 if (items) { 20200 this.add(items); 20201 } 20202 }, 20203 20204 /** 20205 * Adds new items to the control collection. 20206 * 20207 * @method add 20208 * @param {Array} items Array if items to add to collection. 20209 * @return {tinymce.ui.Collection} Current collection instance. 20210 */ 20211 add: function(items) { 20212 var self = this; 20213 20214 // Force single item into array 20215 if (!Tools.isArray(items)) { 20216 if (items instanceof Collection) { 20217 self.add(items.toArray()); 20218 } else { 20219 push.call(self, items); 20220 } 20221 } else { 20222 push.apply(self, items); 20223 } 20224 20225 return self; 20226 }, 20227 20228 /** 20229 * Sets the contents of the collection. This will remove any existing items 20230 * and replace them with the ones specified in the input array. 20231 * 20232 * @method set 20233 * @param {Array} items Array with items to set into the Collection. 20234 * @return {tinymce.ui.Collection} Collection instance. 20235 */ 20236 set: function(items) { 20237 var self = this, len = self.length, i; 20238 20239 self.length = 0; 20240 self.add(items); 20241 20242 // Remove old entries 20243 for (i = self.length; i < len; i++) { 20244 delete self[i]; 20245 } 20246 20247 return self; 20248 }, 20249 20250 /** 20251 * Filters the collection item based on the specified selector expression or selector function. 20252 * 20253 * @method filter 20254 * @param {String} selector Selector expression to filter items by. 20255 * @return {tinymce.ui.Collection} Collection containing the filtered items. 20256 */ 20257 filter: function(selector) { 20258 var self = this, i, l, matches = [], item, match; 20259 20260 // Compile string into selector expression 20261 if (typeof(selector) === "string") { 20262 selector = new Selector(selector); 20263 20264 match = function(item) { 20265 return selector.match(item); 20266 }; 20267 } else { 20268 // Use selector as matching function 20269 match = selector; 20270 } 20271 20272 for (i = 0, l = self.length; i < l; i++) { 20273 item = self[i]; 20274 20275 if (match(item)) { 20276 matches.push(item); 20277 } 20278 } 20279 20280 return new Collection(matches); 20281 }, 20282 20283 /** 20284 * Slices the items within the collection. 20285 * 20286 * @method slice 20287 * @param {Number} index Index to slice at. 20288 * @param {Number} len Optional length to slice. 20289 * @return {tinymce.ui.Collection} Current collection. 20290 */ 20291 slice: function() { 20292 return new Collection(slice.apply(this, arguments)); 20293 }, 20294 20295 /** 20296 * Makes the current collection equal to the specified index. 20297 * 20298 * @method eq 20299 * @param {Number} index Index of the item to set the collection to. 20300 * @return {tinymce.ui.Collection} Current collection. 20301 */ 20302 eq: function(index) { 20303 return index === -1 ? this.slice(index) : this.slice(index, +index + 1); 20304 }, 20305 20306 /** 20307 * Executes the specified callback on each item in collection. 20308 * 20309 * @method each 20310 * @param {function} callback Callback to execute for each item in collection. 20311 * @return {tinymce.ui.Collection} Current collection instance. 20312 */ 20313 each: function(callback) { 20314 Tools.each(this, callback); 20315 20316 return this; 20317 }, 20318 20319 /** 20320 * Returns an JavaScript array object of the contents inside the collection. 20321 * 20322 * @method toArray 20323 * @return {Array} Array with all items from collection. 20324 */ 20325 toArray: function() { 20326 return Tools.toArray(this); 20327 }, 20328 20329 /** 20330 * Finds the index of the specified control or return -1 if it isn't in the collection. 20331 * 20332 * @method indexOf 20333 * @param {Control} ctrl Control instance to look for. 20334 * @return {Number} Index of the specified control or -1. 20335 */ 20336 indexOf: function(ctrl) { 20337 var self = this, i = self.length; 20338 20339 while (i--) { 20340 if (self[i] === ctrl) { 20341 break; 20342 } 20343 } 20344 20345 return i; 20346 }, 20347 20348 /** 20349 * Returns a new collection of the contents in reverse order. 20350 * 20351 * @method reverse 20352 * @return {tinymce.ui.Collection} Collection instance with reversed items. 20353 */ 20354 reverse: function() { 20355 return new Collection(Tools.toArray(this).reverse()); 20356 }, 20357 20358 /** 20359 * Returns true/false if the class exists or not. 20360 * 20361 * @method hasClass 20362 * @param {String} cls Class to check for. 20363 * @return {Boolean} true/false state if the class exists or not. 20364 */ 20365 hasClass: function(cls) { 20366 return this[0] ? this[0].hasClass(cls) : false; 20367 }, 20368 20369 /** 20370 * Sets/gets the specific property on the items in the collection. The same as executing control.<property>(<value>); 20371 * 20372 * @method prop 20373 * @param {String} name Property name to get/set. 20374 * @param {Object} value Optional object value to set. 20375 * @return {tinymce.ui.Collection} Current collection instance or value of the first item on a get operation. 20376 */ 20377 prop: function(name, value) { 20378 var self = this, undef, item; 20379 20380 if (value !== undef) { 20381 self.each(function(item) { 20382 if (item[name]) { 20383 item[name](value); 20384 } 20385 }); 20386 20387 return self; 20388 } 20389 20390 item = self[0]; 20391 20392 if (item && item[name]) { 20393 return item[name](); 20394 } 20395 }, 20396 20397 /** 20398 * Executes the specific function name with optional arguments an all items in collection if it exists. 20399 * 20400 * @example collection.exec("myMethod", arg1, arg2, arg3); 20401 * @method exec 20402 * @param {String} name Name of the function to execute. 20403 * @param {Object} ... Multiple arguments to pass to each function. 20404 * @return {tinymce.ui.Collection} Current collection. 20405 */ 20406 exec: function(name) { 20407 var self = this, args = Tools.toArray(arguments).slice(1); 20408 20409 self.each(function(item) { 20410 if (item[name]) { 20411 item[name].apply(item, args); 20412 } 20413 }); 20414 20415 return self; 20416 }, 20417 20418 /** 20419 * Remove all items from collection and DOM. 20420 * 20421 * @method remove 20422 * @return {tinymce.ui.Collection} Current collection. 20423 */ 20424 remove: function() { 20425 var i = this.length; 20426 20427 while (i--) { 20428 this[i].remove(); 20429 } 20430 20431 return this; 20432 } 20433 20434 /** 20435 * Fires the specified event by name and arguments on the control. This will execute all 20436 * bound event handlers. 20437 * 20438 * @method fire 20439 * @param {String} name Name of the event to fire. 20440 * @param {Object} args Optional arguments to pass to the event. 20441 * @return {tinymce.ui.Collection} Current collection instance. 20442 */ 20443 // fire: function(event, args) {}, -- Generated by code below 20444 20445 /** 20446 * Binds a callback to the specified event. This event can both be 20447 * native browser events like "click" or custom ones like PostRender. 20448 * 20449 * The callback function will have two parameters the first one being the control that received the event 20450 * the second one will be the event object either the browsers native event object or a custom JS object. 20451 * 20452 * @method on 20453 * @param {String} name Name of the event to bind. For example "click". 20454 * @param {String/function} callback Callback function to execute ones the event occurs. 20455 * @return {tinymce.ui.Collection} Current collection instance. 20456 */ 20457 // on: function(name, callback) {}, -- Generated by code below 20458 20459 /** 20460 * Unbinds the specified event and optionally a specific callback. If you omit the name 20461 * parameter all event handlers will be removed. If you omit the callback all event handles 20462 * by the specified name will be removed. 20463 * 20464 * @method off 20465 * @param {String} name Optional name for the event to unbind. 20466 * @param {function} callback Optional callback function to unbind. 20467 * @return {tinymce.ui.Collection} Current collection instance. 20468 */ 20469 // off: function(name, callback) {}, -- Generated by code below 20470 20471 /** 20472 * Shows the items in the current collection. 20473 * 20474 * @method show 20475 * @return {tinymce.ui.Collection} Current collection instance. 20476 */ 20477 // show: function() {}, -- Generated by code below 20478 20479 /** 20480 * Hides the items in the current collection. 20481 * 20482 * @method hide 20483 * @return {tinymce.ui.Collection} Current collection instance. 20484 */ 20485 // hide: function() {}, -- Generated by code below 20486 20487 /** 20488 * Sets/gets the text contents of the items in the current collection. 20489 * 20490 * @method text 20491 * @return {tinymce.ui.Collection} Current collection instance or text value of the first item on a get operation. 20492 */ 20493 // text: function(value) {}, -- Generated by code below 20494 20495 /** 20496 * Sets/gets the name contents of the items in the current collection. 20497 * 20498 * @method name 20499 * @return {tinymce.ui.Collection} Current collection instance or name value of the first item on a get operation. 20500 */ 20501 // name: function(value) {}, -- Generated by code below 20502 20503 /** 20504 * Sets/gets the disabled state on the items in the current collection. 20505 * 20506 * @method disabled 20507 * @return {tinymce.ui.Collection} Current collection instance or disabled state of the first item on a get operation. 20508 */ 20509 // disabled: function(state) {}, -- Generated by code below 20510 20511 /** 20512 * Sets/gets the active state on the items in the current collection. 20513 * 20514 * @method active 20515 * @return {tinymce.ui.Collection} Current collection instance or active state of the first item on a get operation. 20516 */ 20517 // active: function(state) {}, -- Generated by code below 20518 20519 /** 20520 * Sets/gets the selected state on the items in the current collection. 20521 * 20522 * @method selected 20523 * @return {tinymce.ui.Collection} Current collection instance or selected state of the first item on a get operation. 20524 */ 20525 // selected: function(state) {}, -- Generated by code below 20526 20527 /** 20528 * Sets/gets the selected state on the items in the current collection. 20529 * 20530 * @method visible 20531 * @return {tinymce.ui.Collection} Current collection instance or visible state of the first item on a get operation. 20532 */ 20533 // visible: function(state) {}, -- Generated by code below 20534 20535 /** 20536 * Adds a class to all items in the collection. 20537 * 20538 * @method addClass 20539 * @param {String} cls Class to add to each item. 20540 * @return {tinymce.ui.Collection} Current collection instance. 20541 */ 20542 // addClass: function(cls) {}, -- Generated by code below 20543 20544 /** 20545 * Removes the specified class from all items in collection. 20546 * 20547 * @method removeClass 20548 * @param {String} cls Class to remove from each item. 20549 * @return {tinymce.ui.Collection} Current collection instance. 20550 */ 20551 // removeClass: function(cls) {}, -- Generated by code below 20552 }; 20553 20554 // Extend tinymce.ui.Collection prototype with some generated control specific methods 20555 Tools.each('fire on off show hide addClass removeClass append prepend before after reflow'.split(' '), function(name) { 20556 proto[name] = function() { 20557 var args = Tools.toArray(arguments); 20558 20559 this.each(function(ctrl) { 20560 if (name in ctrl) { 20561 ctrl[name].apply(ctrl, args); 20562 } 20563 }); 20564 20565 return this; 20566 }; 20567 }); 20568 20569 // Extend tinymce.ui.Collection prototype with some property methods 20570 Tools.each('text name disabled active selected checked visible parent value data'.split(' '), function(name) { 20571 proto[name] = function(value) { 20572 return this.prop(name, value); 20573 }; 20574 }); 20575 20576 // Create class based on the new prototype 20577 Collection = Class.extend(proto); 20578 20579 // Stick Collection into Selector to prevent circual references 20580 Selector.Collection = Collection; 20581 20582 return Collection; 20583 }); 20584 20585 // Included from: js/tinymce/classes/ui/DomUtils.js 20586 20587 /** 20588 * DOMUtils.js 20589 * 20590 * Copyright, Moxiecode Systems AB 20591 * Released under LGPL License. 20592 * 20593 * License: http://www.tinymce.com/license 20594 * Contributing: http://www.tinymce.com/contributing 20595 */ 20596 20597 define("tinymce/ui/DomUtils", [ 20598 "tinymce/util/Tools", 20599 "tinymce/dom/DOMUtils" 20600 ], function(Tools, DOMUtils) { 20601 "use strict"; 20602 20603 return { 20604 id: function() { 20605 return DOMUtils.DOM.uniqueId(); 20606 }, 20607 20608 createFragment: function(html) { 20609 return DOMUtils.DOM.createFragment(html); 20610 }, 20611 20612 getWindowSize: function() { 20613 return DOMUtils.DOM.getViewPort(); 20614 }, 20615 20616 getSize: function(elm) { 20617 var width, height; 20618 20619 if (elm.getBoundingClientRect) { 20620 var rect = elm.getBoundingClientRect(); 20621 20622 width = Math.max(rect.width || (rect.right - rect.left), elm.offsetWidth); 20623 height = Math.max(rect.height || (rect.bottom - rect.bottom), elm.offsetHeight); 20624 } else { 20625 width = elm.offsetWidth; 20626 height = elm.offsetHeight; 20627 } 20628 20629 return {width: width, height: height}; 20630 }, 20631 20632 getPos: function(elm, root) { 20633 return DOMUtils.DOM.getPos(elm, root); 20634 }, 20635 20636 getViewPort: function(win) { 20637 return DOMUtils.DOM.getViewPort(win); 20638 }, 20639 20640 get: function(id) { 20641 return document.getElementById(id); 20642 }, 20643 20644 addClass : function(elm, cls) { 20645 return DOMUtils.DOM.addClass(elm, cls); 20646 }, 20647 20648 removeClass : function(elm, cls) { 20649 return DOMUtils.DOM.removeClass(elm, cls); 20650 }, 20651 20652 hasClass : function(elm, cls) { 20653 return DOMUtils.DOM.hasClass(elm, cls); 20654 }, 20655 20656 toggleClass: function(elm, cls, state) { 20657 return DOMUtils.DOM.toggleClass(elm, cls, state); 20658 }, 20659 20660 css: function(elm, name, value) { 20661 return DOMUtils.DOM.setStyle(elm, name, value); 20662 }, 20663 20664 on: function(target, name, callback, scope) { 20665 return DOMUtils.DOM.bind(target, name, callback, scope); 20666 }, 20667 20668 off: function(target, name, callback) { 20669 return DOMUtils.DOM.unbind(target, name, callback); 20670 }, 20671 20672 fire: function(target, name, args) { 20673 return DOMUtils.DOM.fire(target, name, args); 20674 }, 20675 20676 innerHtml: function(elm, html) { 20677 // Workaround for <div> in <p> bug on IE 8 #6178 20678 DOMUtils.DOM.setHTML(elm, html); 20679 } 20680 }; 20681 }); 20682 20683 // Included from: js/tinymce/classes/ui/Control.js 20684 20685 /** 20686 * Control.js 20687 * 20688 * Copyright, Moxiecode Systems AB 20689 * Released under LGPL License. 20690 * 20691 * License: http://www.tinymce.com/license 20692 * Contributing: http://www.tinymce.com/contributing 20693 */ 20694 20695 /*eslint consistent-this:0 */ 20696 20697 /** 20698 * This is the base class for all controls and containers. All UI control instances inherit 20699 * from this one as it has the base logic needed by all of them. 20700 * 20701 * @class tinymce.ui.Control 20702 */ 20703 define("tinymce/ui/Control", [ 20704 "tinymce/util/Class", 20705 "tinymce/util/Tools", 20706 "tinymce/util/EventDispatcher", 20707 "tinymce/ui/Collection", 20708 "tinymce/ui/DomUtils" 20709 ], function(Class, Tools, EventDispatcher, Collection, DomUtils) { 20710 "use strict"; 20711 20712 var elementIdCache = {}; 20713 var hasMouseWheelEventSupport = "onmousewheel" in document; 20714 var hasWheelEventSupport = false; 20715 var classPrefix = "mce-"; 20716 20717 function getEventDispatcher(obj) { 20718 if (!obj._eventDispatcher) { 20719 obj._eventDispatcher = new EventDispatcher({ 20720 scope: obj, 20721 toggleEvent: function(name, state) { 20722 if (state && EventDispatcher.isNative(name)) { 20723 if (!obj._nativeEvents) { 20724 obj._nativeEvents = {}; 20725 } 20726 20727 obj._nativeEvents[name] = true; 20728 20729 if (obj._rendered) { 20730 obj.bindPendingEvents(); 20731 } 20732 } 20733 } 20734 }); 20735 } 20736 20737 return obj._eventDispatcher; 20738 } 20739 20740 var Control = Class.extend({ 20741 Statics: { 20742 elementIdCache: elementIdCache, 20743 classPrefix: classPrefix 20744 }, 20745 20746 isRtl: function() { 20747 return Control.rtl; 20748 }, 20749 20750 /** 20751 * Class/id prefix to use for all controls. 20752 * 20753 * @final 20754 * @field {String} classPrefix 20755 */ 20756 classPrefix: classPrefix, 20757 20758 /** 20759 * Constructs a new control instance with the specified settings. 20760 * 20761 * @constructor 20762 * @param {Object} settings Name/value object with settings. 20763 * @setting {String} style Style CSS properties to add. 20764 * @setting {String} border Border box values example: 1 1 1 1 20765 * @setting {String} padding Padding box values example: 1 1 1 1 20766 * @setting {String} margin Margin box values example: 1 1 1 1 20767 * @setting {Number} minWidth Minimal width for the control. 20768 * @setting {Number} minHeight Minimal height for the control. 20769 * @setting {String} classes Space separated list of classes to add. 20770 * @setting {String} role WAI-ARIA role to use for control. 20771 * @setting {Boolean} hidden Is the control hidden by default. 20772 * @setting {Boolean} disabled Is the control disabled by default. 20773 * @setting {String} name Name of the control instance. 20774 */ 20775 init: function(settings) { 20776 var self = this, classes, i; 20777 20778 self.settings = settings = Tools.extend({}, self.Defaults, settings); 20779 20780 // Initial states 20781 self._id = settings.id || DomUtils.id(); 20782 self._text = self._name = ''; 20783 self._width = self._height = 0; 20784 self._aria = {role: settings.role}; 20785 20786 // Setup classes 20787 classes = settings.classes; 20788 if (classes) { 20789 classes = classes.split(' '); 20790 classes.map = {}; 20791 i = classes.length; 20792 while (i--) { 20793 classes.map[classes[i]] = true; 20794 } 20795 } 20796 20797 self._classes = classes || []; 20798 self.visible(true); 20799 20800 // Set some properties 20801 Tools.each('title text width height name classes visible disabled active value'.split(' '), function(name) { 20802 var value = settings[name], undef; 20803 20804 if (value !== undef) { 20805 self[name](value); 20806 } else if (self['_' + name] === undef) { 20807 self['_' + name] = false; 20808 } 20809 }); 20810 20811 self.on('click', function() { 20812 if (self.disabled()) { 20813 return false; 20814 } 20815 }); 20816 20817 // TODO: Is this needed duplicate code see above? 20818 if (settings.classes) { 20819 Tools.each(settings.classes.split(' '), function(cls) { 20820 self.addClass(cls); 20821 }); 20822 } 20823 20824 /** 20825 * Name/value object with settings for the current control. 20826 * 20827 * @field {Object} settings 20828 */ 20829 self.settings = settings; 20830 20831 self._borderBox = self.parseBox(settings.border); 20832 self._paddingBox = self.parseBox(settings.padding); 20833 self._marginBox = self.parseBox(settings.margin); 20834 20835 if (settings.hidden) { 20836 self.hide(); 20837 } 20838 }, 20839 20840 // Will generate getter/setter methods for these properties 20841 Properties: 'parent,title,text,width,height,disabled,active,name,value', 20842 20843 // Will generate empty dummy functions for these 20844 Methods: 'renderHtml', 20845 20846 /** 20847 * Returns the root element to render controls into. 20848 * 20849 * @method getContainerElm 20850 * @return {Element} HTML DOM element to render into. 20851 */ 20852 getContainerElm: function() { 20853 return document.body; 20854 }, 20855 20856 /** 20857 * Returns a control instance for the current DOM element. 20858 * 20859 * @method getParentCtrl 20860 * @param {Element} elm HTML dom element to get parent control from. 20861 * @return {tinymce.ui.Control} Control instance or undefined. 20862 */ 20863 getParentCtrl: function(elm) { 20864 var ctrl, lookup = this.getRoot().controlIdLookup; 20865 20866 while (elm && lookup) { 20867 ctrl = lookup[elm.id]; 20868 if (ctrl) { 20869 break; 20870 } 20871 20872 elm = elm.parentNode; 20873 } 20874 20875 return ctrl; 20876 }, 20877 20878 /** 20879 * Parses the specified box value. A box value contains 1-4 properties in clockwise order. 20880 * 20881 * @method parseBox 20882 * @param {String/Number} value Box value "0 1 2 3" or "0" etc. 20883 * @return {Object} Object with top/right/bottom/left properties. 20884 * @private 20885 */ 20886 parseBox: function(value) { 20887 var len, radix = 10; 20888 20889 if (!value) { 20890 return; 20891 } 20892 20893 if (typeof(value) === "number") { 20894 value = value || 0; 20895 20896 return { 20897 top: value, 20898 left: value, 20899 bottom: value, 20900 right: value 20901 }; 20902 } 20903 20904 value = value.split(' '); 20905 len = value.length; 20906 20907 if (len === 1) { 20908 value[1] = value[2] = value[3] = value[0]; 20909 } else if (len === 2) { 20910 value[2] = value[0]; 20911 value[3] = value[1]; 20912 } else if (len === 3) { 20913 value[3] = value[1]; 20914 } 20915 20916 return { 20917 top: parseInt(value[0], radix) || 0, 20918 right: parseInt(value[1], radix) || 0, 20919 bottom: parseInt(value[2], radix) || 0, 20920 left: parseInt(value[3], radix) || 0 20921 }; 20922 }, 20923 20924 borderBox: function() { 20925 return this._borderBox; 20926 }, 20927 20928 paddingBox: function() { 20929 return this._paddingBox; 20930 }, 20931 20932 marginBox: function() { 20933 return this._marginBox; 20934 }, 20935 20936 measureBox: function(elm, prefix) { 20937 function getStyle(name) { 20938 var defaultView = document.defaultView; 20939 20940 if (defaultView) { 20941 // Remove camelcase 20942 name = name.replace(/[A-Z]/g, function(a) { 20943 return '-' + a; 20944 }); 20945 20946 return defaultView.getComputedStyle(elm, null).getPropertyValue(name); 20947 } 20948 20949 return elm.currentStyle[name]; 20950 } 20951 20952 function getSide(name) { 20953 var val = parseFloat(getStyle(name), 10); 20954 20955 return isNaN(val) ? 0 : val; 20956 } 20957 20958 return { 20959 top: getSide(prefix + "TopWidth"), 20960 right: getSide(prefix + "RightWidth"), 20961 bottom: getSide(prefix + "BottomWidth"), 20962 left: getSide(prefix + "LeftWidth") 20963 }; 20964 }, 20965 20966 /** 20967 * Initializes the current controls layout rect. 20968 * This will be executed by the layout managers to determine the 20969 * default minWidth/minHeight etc. 20970 * 20971 * @method initLayoutRect 20972 * @return {Object} Layout rect instance. 20973 */ 20974 initLayoutRect: function() { 20975 var self = this, settings = self.settings, borderBox, layoutRect; 20976 var elm = self.getEl(), width, height, minWidth, minHeight, autoResize; 20977 var startMinWidth, startMinHeight, initialSize; 20978 20979 // Measure the current element 20980 borderBox = self._borderBox = self._borderBox || self.measureBox(elm, 'border'); 20981 self._paddingBox = self._paddingBox || self.measureBox(elm, 'padding'); 20982 self._marginBox = self._marginBox || self.measureBox(elm, 'margin'); 20983 initialSize = DomUtils.getSize(elm); 20984 20985 // Setup minWidth/minHeight and width/height 20986 startMinWidth = settings.minWidth; 20987 startMinHeight = settings.minHeight; 20988 minWidth = startMinWidth || initialSize.width; 20989 minHeight = startMinHeight || initialSize.height; 20990 width = settings.width; 20991 height = settings.height; 20992 autoResize = settings.autoResize; 20993 autoResize = typeof(autoResize) != "undefined" ? autoResize : !width && !height; 20994 20995 width = width || minWidth; 20996 height = height || minHeight; 20997 20998 var deltaW = borderBox.left + borderBox.right; 20999 var deltaH = borderBox.top + borderBox.bottom; 21000 21001 var maxW = settings.maxWidth || 0xFFFF; 21002 var maxH = settings.maxHeight || 0xFFFF; 21003 21004 // Setup initial layout rect 21005 self._layoutRect = layoutRect = { 21006 x: settings.x || 0, 21007 y: settings.y || 0, 21008 w: width, 21009 h: height, 21010 deltaW: deltaW, 21011 deltaH: deltaH, 21012 contentW: width - deltaW, 21013 contentH: height - deltaH, 21014 innerW: width - deltaW, 21015 innerH: height - deltaH, 21016 startMinWidth: startMinWidth || 0, 21017 startMinHeight: startMinHeight || 0, 21018 minW: Math.min(minWidth, maxW), 21019 minH: Math.min(minHeight, maxH), 21020 maxW: maxW, 21021 maxH: maxH, 21022 autoResize: autoResize, 21023 scrollW: 0 21024 }; 21025 21026 self._lastLayoutRect = {}; 21027 21028 return layoutRect; 21029 }, 21030 21031 /** 21032 * Getter/setter for the current layout rect. 21033 * 21034 * @method layoutRect 21035 * @param {Object} [newRect] Optional new layout rect. 21036 * @return {tinymce.ui.Control/Object} Current control or rect object. 21037 */ 21038 layoutRect: function(newRect) { 21039 var self = this, curRect = self._layoutRect, lastLayoutRect, size, deltaWidth, deltaHeight, undef, repaintControls; 21040 21041 // Initialize default layout rect 21042 if (!curRect) { 21043 curRect = self.initLayoutRect(); 21044 } 21045 21046 // Set new rect values 21047 if (newRect) { 21048 // Calc deltas between inner and outer sizes 21049 deltaWidth = curRect.deltaW; 21050 deltaHeight = curRect.deltaH; 21051 21052 // Set x position 21053 if (newRect.x !== undef) { 21054 curRect.x = newRect.x; 21055 } 21056 21057 // Set y position 21058 if (newRect.y !== undef) { 21059 curRect.y = newRect.y; 21060 } 21061 21062 // Set minW 21063 if (newRect.minW !== undef) { 21064 curRect.minW = newRect.minW; 21065 } 21066 21067 // Set minH 21068 if (newRect.minH !== undef) { 21069 curRect.minH = newRect.minH; 21070 } 21071 21072 // Set new width and calculate inner width 21073 size = newRect.w; 21074 if (size !== undef) { 21075 size = size < curRect.minW ? curRect.minW : size; 21076 size = size > curRect.maxW ? curRect.maxW : size; 21077 curRect.w = size; 21078 curRect.innerW = size - deltaWidth; 21079 } 21080 21081 // Set new height and calculate inner height 21082 size = newRect.h; 21083 if (size !== undef) { 21084 size = size < curRect.minH ? curRect.minH : size; 21085 size = size > curRect.maxH ? curRect.maxH : size; 21086 curRect.h = size; 21087 curRect.innerH = size - deltaHeight; 21088 } 21089 21090 // Set new inner width and calculate width 21091 size = newRect.innerW; 21092 if (size !== undef) { 21093 size = size < curRect.minW - deltaWidth ? curRect.minW - deltaWidth : size; 21094 size = size > curRect.maxW - deltaWidth ? curRect.maxW - deltaWidth : size; 21095 curRect.innerW = size; 21096 curRect.w = size + deltaWidth; 21097 } 21098 21099 // Set new height and calculate inner height 21100 size = newRect.innerH; 21101 if (size !== undef) { 21102 size = size < curRect.minH - deltaHeight ? curRect.minH - deltaHeight : size; 21103 size = size > curRect.maxH - deltaHeight ? curRect.maxH - deltaHeight : size; 21104 curRect.innerH = size; 21105 curRect.h = size + deltaHeight; 21106 } 21107 21108 // Set new contentW 21109 if (newRect.contentW !== undef) { 21110 curRect.contentW = newRect.contentW; 21111 } 21112 21113 // Set new contentH 21114 if (newRect.contentH !== undef) { 21115 curRect.contentH = newRect.contentH; 21116 } 21117 21118 // Compare last layout rect with the current one to see if we need to repaint or not 21119 lastLayoutRect = self._lastLayoutRect; 21120 if (lastLayoutRect.x !== curRect.x || lastLayoutRect.y !== curRect.y || 21121 lastLayoutRect.w !== curRect.w || lastLayoutRect.h !== curRect.h) { 21122 repaintControls = Control.repaintControls; 21123 21124 if (repaintControls) { 21125 if (repaintControls.map && !repaintControls.map[self._id]) { 21126 repaintControls.push(self); 21127 repaintControls.map[self._id] = true; 21128 } 21129 } 21130 21131 lastLayoutRect.x = curRect.x; 21132 lastLayoutRect.y = curRect.y; 21133 lastLayoutRect.w = curRect.w; 21134 lastLayoutRect.h = curRect.h; 21135 } 21136 21137 return self; 21138 } 21139 21140 return curRect; 21141 }, 21142 21143 /** 21144 * Repaints the control after a layout operation. 21145 * 21146 * @method repaint 21147 */ 21148 repaint: function() { 21149 var self = this, style, bodyStyle, rect, borderBox, borderW = 0, borderH = 0, lastRepaintRect, round; 21150 21151 // Use Math.round on all values on IE < 9 21152 round = !document.createRange ? Math.round : function(value) { 21153 return value; 21154 }; 21155 21156 style = self.getEl().style; 21157 rect = self._layoutRect; 21158 lastRepaintRect = self._lastRepaintRect || {}; 21159 21160 borderBox = self._borderBox; 21161 borderW = borderBox.left + borderBox.right; 21162 borderH = borderBox.top + borderBox.bottom; 21163 21164 if (rect.x !== lastRepaintRect.x) { 21165 style.left = round(rect.x) + 'px'; 21166 lastRepaintRect.x = rect.x; 21167 } 21168 21169 if (rect.y !== lastRepaintRect.y) { 21170 style.top = round(rect.y) + 'px'; 21171 lastRepaintRect.y = rect.y; 21172 } 21173 21174 if (rect.w !== lastRepaintRect.w) { 21175 style.width = round(rect.w - borderW) + 'px'; 21176 lastRepaintRect.w = rect.w; 21177 } 21178 21179 if (rect.h !== lastRepaintRect.h) { 21180 style.height = round(rect.h - borderH) + 'px'; 21181 lastRepaintRect.h = rect.h; 21182 } 21183 21184 // Update body if needed 21185 if (self._hasBody && rect.innerW !== lastRepaintRect.innerW) { 21186 bodyStyle = self.getEl('body').style; 21187 bodyStyle.width = round(rect.innerW) + 'px'; 21188 lastRepaintRect.innerW = rect.innerW; 21189 } 21190 21191 if (self._hasBody && rect.innerH !== lastRepaintRect.innerH) { 21192 bodyStyle = bodyStyle || self.getEl('body').style; 21193 bodyStyle.height = round(rect.innerH) + 'px'; 21194 lastRepaintRect.innerH = rect.innerH; 21195 } 21196 21197 self._lastRepaintRect = lastRepaintRect; 21198 self.fire('repaint', {}, false); 21199 }, 21200 21201 /** 21202 * Binds a callback to the specified event. This event can both be 21203 * native browser events like "click" or custom ones like PostRender. 21204 * 21205 * The callback function will be passed a DOM event like object that enables yout do stop propagation. 21206 * 21207 * @method on 21208 * @param {String} name Name of the event to bind. For example "click". 21209 * @param {String/function} callback Callback function to execute ones the event occurs. 21210 * @return {tinymce.ui.Control} Current control object. 21211 */ 21212 on: function(name, callback) { 21213 var self = this; 21214 21215 function resolveCallbackName(name) { 21216 var callback, scope; 21217 21218 if (typeof(name) != 'string') { 21219 return name; 21220 } 21221 21222 return function(e) { 21223 if (!callback) { 21224 self.parentsAndSelf().each(function(ctrl) { 21225 var callbacks = ctrl.settings.callbacks; 21226 21227 if (callbacks && (callback = callbacks[name])) { 21228 scope = ctrl; 21229 return false; 21230 } 21231 }); 21232 } 21233 21234 return callback.call(scope, e); 21235 }; 21236 } 21237 21238 getEventDispatcher(self).on(name, resolveCallbackName(callback)); 21239 21240 return self; 21241 }, 21242 21243 /** 21244 * Unbinds the specified event and optionally a specific callback. If you omit the name 21245 * parameter all event handlers will be removed. If you omit the callback all event handles 21246 * by the specified name will be removed. 21247 * 21248 * @method off 21249 * @param {String} [name] Name for the event to unbind. 21250 * @param {function} [callback] Callback function to unbind. 21251 * @return {mxex.ui.Control} Current control object. 21252 */ 21253 off: function(name, callback) { 21254 getEventDispatcher(this).off(name, callback); 21255 return this; 21256 }, 21257 21258 /** 21259 * Fires the specified event by name and arguments on the control. This will execute all 21260 * bound event handlers. 21261 * 21262 * @method fire 21263 * @param {String} name Name of the event to fire. 21264 * @param {Object} [args] Arguments to pass to the event. 21265 * @param {Boolean} [bubble] Value to control bubbeling. Defaults to true. 21266 * @return {Object} Current arguments object. 21267 */ 21268 fire: function(name, args, bubble) { 21269 var self = this; 21270 21271 args = args || {}; 21272 21273 if (!args.control) { 21274 args.control = self; 21275 } 21276 21277 args = getEventDispatcher(self).fire(name, args); 21278 21279 // Bubble event up to parents 21280 if (bubble !== false && self.parent) { 21281 var parent = self.parent(); 21282 while (parent && !args.isPropagationStopped()) { 21283 parent.fire(name, args, false); 21284 parent = parent.parent(); 21285 } 21286 } 21287 21288 return args; 21289 }, 21290 21291 /** 21292 * Returns true/false if the specified event has any listeners. 21293 * 21294 * @method hasEventListeners 21295 * @param {String} name Name of the event to check for. 21296 * @return {Boolean} True/false state if the event has listeners. 21297 */ 21298 hasEventListeners: function(name) { 21299 return getEventDispatcher(this).has(name); 21300 }, 21301 21302 /** 21303 * Returns a control collection with all parent controls. 21304 * 21305 * @method parents 21306 * @param {String} selector Optional selector expression to find parents. 21307 * @return {tinymce.ui.Collection} Collection with all parent controls. 21308 */ 21309 parents: function(selector) { 21310 var self = this, ctrl, parents = new Collection(); 21311 21312 // Add each parent to collection 21313 for (ctrl = self.parent(); ctrl; ctrl = ctrl.parent()) { 21314 parents.add(ctrl); 21315 } 21316 21317 // Filter away everything that doesn't match the selector 21318 if (selector) { 21319 parents = parents.filter(selector); 21320 } 21321 21322 return parents; 21323 }, 21324 21325 /** 21326 * Returns the current control and it's parents. 21327 * 21328 * @method parentsAndSelf 21329 * @param {String} selector Optional selector expression to find parents. 21330 * @return {tinymce.ui.Collection} Collection with all parent controls. 21331 */ 21332 parentsAndSelf: function(selector) { 21333 return new Collection(this).add(this.parents(selector)); 21334 }, 21335 21336 /** 21337 * Returns the control next to the current control. 21338 * 21339 * @method next 21340 * @return {tinymce.ui.Control} Next control instance. 21341 */ 21342 next: function() { 21343 var parentControls = this.parent().items(); 21344 21345 return parentControls[parentControls.indexOf(this) + 1]; 21346 }, 21347 21348 /** 21349 * Returns the control previous to the current control. 21350 * 21351 * @method prev 21352 * @return {tinymce.ui.Control} Previous control instance. 21353 */ 21354 prev: function() { 21355 var parentControls = this.parent().items(); 21356 21357 return parentControls[parentControls.indexOf(this) - 1]; 21358 }, 21359 21360 /** 21361 * Find the common ancestor for two control instances. 21362 * 21363 * @method findCommonAncestor 21364 * @param {tinymce.ui.Control} ctrl1 First control. 21365 * @param {tinymce.ui.Control} ctrl2 Second control. 21366 * @return {tinymce.ui.Control} Ancestor control instance. 21367 */ 21368 findCommonAncestor: function(ctrl1, ctrl2) { 21369 var parentCtrl; 21370 21371 while (ctrl1) { 21372 parentCtrl = ctrl2; 21373 21374 while (parentCtrl && ctrl1 != parentCtrl) { 21375 parentCtrl = parentCtrl.parent(); 21376 } 21377 21378 if (ctrl1 == parentCtrl) { 21379 break; 21380 } 21381 21382 ctrl1 = ctrl1.parent(); 21383 } 21384 21385 return ctrl1; 21386 }, 21387 21388 /** 21389 * Returns true/false if the specific control has the specific class. 21390 * 21391 * @method hasClass 21392 * @param {String} cls Class to check for. 21393 * @param {String} [group] Sub element group name. 21394 * @return {Boolean} True/false if the control has the specified class. 21395 */ 21396 hasClass: function(cls, group) { 21397 var classes = this._classes[group || 'control']; 21398 21399 cls = this.classPrefix + cls; 21400 21401 return classes && !!classes.map[cls]; 21402 }, 21403 21404 /** 21405 * Adds the specified class to the control 21406 * 21407 * @method addClass 21408 * @param {String} cls Class to check for. 21409 * @param {String} [group] Sub element group name. 21410 * @return {tinymce.ui.Control} Current control object. 21411 */ 21412 addClass: function(cls, group) { 21413 var self = this, classes, elm; 21414 21415 cls = this.classPrefix + cls; 21416 classes = self._classes[group || 'control']; 21417 21418 if (!classes) { 21419 classes = []; 21420 classes.map = {}; 21421 self._classes[group || 'control'] = classes; 21422 } 21423 21424 if (!classes.map[cls]) { 21425 classes.map[cls] = cls; 21426 classes.push(cls); 21427 21428 if (self._rendered) { 21429 elm = self.getEl(group); 21430 21431 if (elm) { 21432 elm.className = classes.join(' '); 21433 } 21434 } 21435 } 21436 21437 return self; 21438 }, 21439 21440 /** 21441 * Removes the specified class from the control. 21442 * 21443 * @method removeClass 21444 * @param {String} cls Class to remove. 21445 * @param {String} [group] Sub element group name. 21446 * @return {tinymce.ui.Control} Current control object. 21447 */ 21448 removeClass: function(cls, group) { 21449 var self = this, classes, i, elm; 21450 21451 cls = this.classPrefix + cls; 21452 classes = self._classes[group || 'control']; 21453 if (classes && classes.map[cls]) { 21454 delete classes.map[cls]; 21455 21456 i = classes.length; 21457 while (i--) { 21458 if (classes[i] === cls) { 21459 classes.splice(i, 1); 21460 } 21461 } 21462 } 21463 21464 if (self._rendered) { 21465 elm = self.getEl(group); 21466 21467 if (elm) { 21468 elm.className = classes.join(' '); 21469 } 21470 } 21471 21472 return self; 21473 }, 21474 21475 /** 21476 * Toggles the specified class on the control. 21477 * 21478 * @method toggleClass 21479 * @param {String} cls Class to remove. 21480 * @param {Boolean} state True/false state to add/remove class. 21481 * @param {String} [group] Sub element group name. 21482 * @return {tinymce.ui.Control} Current control object. 21483 */ 21484 toggleClass: function(cls, state, group) { 21485 var self = this; 21486 21487 if (state) { 21488 self.addClass(cls, group); 21489 } else { 21490 self.removeClass(cls, group); 21491 } 21492 21493 return self; 21494 }, 21495 21496 /** 21497 * Returns the class string for the specified group name. 21498 * 21499 * @method classes 21500 * @param {String} [group] Group to get clases by. 21501 * @return {String} Classes for the specified group. 21502 */ 21503 classes: function(group) { 21504 var classes = this._classes[group || 'control']; 21505 21506 return classes ? classes.join(' ') : ''; 21507 }, 21508 21509 /** 21510 * Sets the inner HTML of the control element. 21511 * 21512 * @method innerHtml 21513 * @param {String} html Html string to set as inner html. 21514 * @return {tinymce.ui.Control} Current control object. 21515 */ 21516 innerHtml: function(html) { 21517 DomUtils.innerHtml(this.getEl(), html); 21518 return this; 21519 }, 21520 21521 /** 21522 * Returns the control DOM element or sub element. 21523 * 21524 * @method getEl 21525 * @param {String} [suffix] Suffix to get element by. 21526 * @param {Boolean} [dropCache] True if the cache for the element should be dropped. 21527 * @return {Element} HTML DOM element for the current control or it's children. 21528 */ 21529 getEl: function(suffix, dropCache) { 21530 var elm, id = suffix ? this._id + '-' + suffix : this._id; 21531 21532 elm = elementIdCache[id] = (dropCache === true ? null : elementIdCache[id]) || DomUtils.get(id); 21533 21534 return elm; 21535 }, 21536 21537 /** 21538 * Sets/gets the visible for the control. 21539 * 21540 * @method visible 21541 * @param {Boolean} state Value to set to control. 21542 * @return {Boolean/tinymce.ui.Control} Current control on a set operation or current state on a get. 21543 */ 21544 visible: function(state) { 21545 var self = this, parentCtrl; 21546 21547 if (typeof(state) !== "undefined") { 21548 if (self._visible !== state) { 21549 if (self._rendered) { 21550 self.getEl().style.display = state ? '' : 'none'; 21551 } 21552 21553 self._visible = state; 21554 21555 // Parent container needs to reflow 21556 parentCtrl = self.parent(); 21557 if (parentCtrl) { 21558 parentCtrl._lastRect = null; 21559 } 21560 21561 self.fire(state ? 'show' : 'hide'); 21562 } 21563 21564 return self; 21565 } 21566 21567 return self._visible; 21568 }, 21569 21570 /** 21571 * Sets the visible state to true. 21572 * 21573 * @method show 21574 * @return {tinymce.ui.Control} Current control instance. 21575 */ 21576 show: function() { 21577 return this.visible(true); 21578 }, 21579 21580 /** 21581 * Sets the visible state to false. 21582 * 21583 * @method hide 21584 * @return {tinymce.ui.Control} Current control instance. 21585 */ 21586 hide: function() { 21587 return this.visible(false); 21588 }, 21589 21590 /** 21591 * Focuses the current control. 21592 * 21593 * @method focus 21594 * @return {tinymce.ui.Control} Current control instance. 21595 */ 21596 focus: function() { 21597 try { 21598 this.getEl().focus(); 21599 } catch (ex) { 21600 // Ignore IE error 21601 } 21602 21603 return this; 21604 }, 21605 21606 /** 21607 * Blurs the current control. 21608 * 21609 * @method blur 21610 * @return {tinymce.ui.Control} Current control instance. 21611 */ 21612 blur: function() { 21613 this.getEl().blur(); 21614 21615 return this; 21616 }, 21617 21618 /** 21619 * Sets the specified aria property. 21620 * 21621 * @method aria 21622 * @param {String} name Name of the aria property to set. 21623 * @param {String} value Value of the aria property. 21624 * @return {tinymce.ui.Control} Current control instance. 21625 */ 21626 aria: function(name, value) { 21627 var self = this, elm = self.getEl(self.ariaTarget); 21628 21629 if (typeof(value) === "undefined") { 21630 return self._aria[name]; 21631 } else { 21632 self._aria[name] = value; 21633 } 21634 21635 if (self._rendered) { 21636 elm.setAttribute(name == 'role' ? name : 'aria-' + name, value); 21637 } 21638 21639 return self; 21640 }, 21641 21642 /** 21643 * Encodes the specified string with HTML entities. It will also 21644 * translate the string to different languages. 21645 * 21646 * @method encode 21647 * @param {String/Object/Array} text Text to entity encode. 21648 * @param {Boolean} [translate=true] False if the contents shouldn't be translated. 21649 * @return {String} Encoded and possible traslated string. 21650 */ 21651 encode: function(text, translate) { 21652 if (translate !== false) { 21653 text = this.translate(text); 21654 } 21655 21656 return (text || '').replace(/[&<>"]/g, function(match) { 21657 return '' + match.charCodeAt(0) + ';'; 21658 }); 21659 }, 21660 21661 /** 21662 * Returns the translated string. 21663 * 21664 * @method translate 21665 * @param {String} text Text to translate. 21666 * @return {String} Translated string or the same as the input. 21667 */ 21668 translate: function(text) { 21669 return Control.translate ? Control.translate(text) : text; 21670 }, 21671 21672 /** 21673 * Adds items before the current control. 21674 * 21675 * @method before 21676 * @param {Array/tinymce.ui.Collection} items Array of items to prepend before this control. 21677 * @return {tinymce.ui.Control} Current control instance. 21678 */ 21679 before: function(items) { 21680 var self = this, parent = self.parent(); 21681 21682 if (parent) { 21683 parent.insert(items, parent.items().indexOf(self), true); 21684 } 21685 21686 return self; 21687 }, 21688 21689 /** 21690 * Adds items after the current control. 21691 * 21692 * @method after 21693 * @param {Array/tinymce.ui.Collection} items Array of items to append after this control. 21694 * @return {tinymce.ui.Control} Current control instance. 21695 */ 21696 after: function(items) { 21697 var self = this, parent = self.parent(); 21698 21699 if (parent) { 21700 parent.insert(items, parent.items().indexOf(self)); 21701 } 21702 21703 return self; 21704 }, 21705 21706 /** 21707 * Removes the current control from DOM and from UI collections. 21708 * 21709 * @method remove 21710 * @return {tinymce.ui.Control} Current control instance. 21711 */ 21712 remove: function() { 21713 var self = this, elm = self.getEl(), parent = self.parent(), newItems, i; 21714 21715 if (self.items) { 21716 var controls = self.items().toArray(); 21717 i = controls.length; 21718 while (i--) { 21719 controls[i].remove(); 21720 } 21721 } 21722 21723 if (parent && parent.items) { 21724 newItems = []; 21725 21726 parent.items().each(function(item) { 21727 if (item !== self) { 21728 newItems.push(item); 21729 } 21730 }); 21731 21732 parent.items().set(newItems); 21733 parent._lastRect = null; 21734 } 21735 21736 if (self._eventsRoot && self._eventsRoot == self) { 21737 DomUtils.off(elm); 21738 } 21739 21740 var lookup = self.getRoot().controlIdLookup; 21741 if (lookup) { 21742 delete lookup[self._id]; 21743 } 21744 21745 delete elementIdCache[self._id]; 21746 21747 if (elm && elm.parentNode) { 21748 var nodes = elm.getElementsByTagName('*'); 21749 21750 i = nodes.length; 21751 while (i--) { 21752 delete elementIdCache[nodes[i].id]; 21753 } 21754 21755 elm.parentNode.removeChild(elm); 21756 } 21757 21758 self._rendered = false; 21759 21760 return self; 21761 }, 21762 21763 /** 21764 * Renders the control before the specified element. 21765 * 21766 * @method renderBefore 21767 * @param {Element} elm Element to render before. 21768 * @return {tinymce.ui.Control} Current control instance. 21769 */ 21770 renderBefore: function(elm) { 21771 var self = this; 21772 21773 elm.parentNode.insertBefore(DomUtils.createFragment(self.renderHtml()), elm); 21774 self.postRender(); 21775 21776 return self; 21777 }, 21778 21779 /** 21780 * Renders the control to the specified element. 21781 * 21782 * @method renderBefore 21783 * @param {Element} elm Element to render to. 21784 * @return {tinymce.ui.Control} Current control instance. 21785 */ 21786 renderTo: function(elm) { 21787 var self = this; 21788 21789 elm = elm || self.getContainerElm(); 21790 elm.appendChild(DomUtils.createFragment(self.renderHtml())); 21791 self.postRender(); 21792 21793 return self; 21794 }, 21795 21796 /** 21797 * Post render method. Called after the control has been rendered to the target. 21798 * 21799 * @method postRender 21800 * @return {tinymce.ui.Control} Current control instance. 21801 */ 21802 postRender: function() { 21803 var self = this, settings = self.settings, elm, box, parent, name, parentEventsRoot; 21804 21805 // Bind on<event> settings 21806 for (name in settings) { 21807 if (name.indexOf("on") === 0) { 21808 self.on(name.substr(2), settings[name]); 21809 } 21810 } 21811 21812 if (self._eventsRoot) { 21813 for (parent = self.parent(); !parentEventsRoot && parent; parent = parent.parent()) { 21814 parentEventsRoot = parent._eventsRoot; 21815 } 21816 21817 if (parentEventsRoot) { 21818 for (name in parentEventsRoot._nativeEvents) { 21819 self._nativeEvents[name] = true; 21820 } 21821 } 21822 } 21823 21824 self.bindPendingEvents(); 21825 21826 if (settings.style) { 21827 elm = self.getEl(); 21828 if (elm) { 21829 elm.setAttribute('style', settings.style); 21830 elm.style.cssText = settings.style; 21831 } 21832 } 21833 21834 if (!self._visible) { 21835 DomUtils.css(self.getEl(), 'display', 'none'); 21836 } 21837 21838 if (self.settings.border) { 21839 box = self.borderBox(); 21840 DomUtils.css(self.getEl(), { 21841 'border-top-width': box.top, 21842 'border-right-width': box.right, 21843 'border-bottom-width': box.bottom, 21844 'border-left-width': box.left 21845 }); 21846 } 21847 21848 // Add instance to lookup 21849 var root = self.getRoot(); 21850 if (!root.controlIdLookup) { 21851 root.controlIdLookup = {}; 21852 } 21853 21854 root.controlIdLookup[self._id] = self; 21855 21856 for (var key in self._aria) { 21857 self.aria(key, self._aria[key]); 21858 } 21859 21860 self.fire('postrender', {}, false); 21861 }, 21862 21863 /** 21864 * Scrolls the current control into view. 21865 * 21866 * @method scrollIntoView 21867 * @param {String} align Alignment in view top|center|bottom. 21868 * @return {tinymce.ui.Control} Current control instance. 21869 */ 21870 scrollIntoView: function(align) { 21871 function getOffset(elm, rootElm) { 21872 var x, y, parent = elm; 21873 21874 x = y = 0; 21875 while (parent && parent != rootElm && parent.nodeType) { 21876 x += parent.offsetLeft || 0; 21877 y += parent.offsetTop || 0; 21878 parent = parent.offsetParent; 21879 } 21880 21881 return {x: x, y: y}; 21882 } 21883 21884 var elm = this.getEl(), parentElm = elm.parentNode; 21885 var x, y, width, height, parentWidth, parentHeight; 21886 var pos = getOffset(elm, parentElm); 21887 21888 x = pos.x; 21889 y = pos.y; 21890 width = elm.offsetWidth; 21891 height = elm.offsetHeight; 21892 parentWidth = parentElm.clientWidth; 21893 parentHeight = parentElm.clientHeight; 21894 21895 if (align == "end") { 21896 x -= parentWidth - width; 21897 y -= parentHeight - height; 21898 } else if (align == "center") { 21899 x -= (parentWidth / 2) - (width / 2); 21900 y -= (parentHeight / 2) - (height / 2); 21901 } 21902 21903 parentElm.scrollLeft = x; 21904 parentElm.scrollTop = y; 21905 21906 return this; 21907 }, 21908 21909 /** 21910 * Binds pending DOM events. 21911 * 21912 * @private 21913 */ 21914 bindPendingEvents: function() { 21915 var self = this, i, l, parents, eventRootCtrl, nativeEvents, name; 21916 21917 function delegate(e) { 21918 var control = self.getParentCtrl(e.target); 21919 21920 if (control) { 21921 control.fire(e.type, e); 21922 } 21923 } 21924 21925 function mouseLeaveHandler() { 21926 var ctrl = eventRootCtrl._lastHoverCtrl; 21927 21928 if (ctrl) { 21929 ctrl.fire("mouseleave", {target: ctrl.getEl()}); 21930 21931 ctrl.parents().each(function(ctrl) { 21932 ctrl.fire("mouseleave", {target: ctrl.getEl()}); 21933 }); 21934 21935 eventRootCtrl._lastHoverCtrl = null; 21936 } 21937 } 21938 21939 function mouseEnterHandler(e) { 21940 var ctrl = self.getParentCtrl(e.target), lastCtrl = eventRootCtrl._lastHoverCtrl, idx = 0, i, parents, lastParents; 21941 21942 // Over on a new control 21943 if (ctrl !== lastCtrl) { 21944 eventRootCtrl._lastHoverCtrl = ctrl; 21945 21946 parents = ctrl.parents().toArray().reverse(); 21947 parents.push(ctrl); 21948 21949 if (lastCtrl) { 21950 lastParents = lastCtrl.parents().toArray().reverse(); 21951 lastParents.push(lastCtrl); 21952 21953 for (idx = 0; idx < lastParents.length; idx++) { 21954 if (parents[idx] !== lastParents[idx]) { 21955 break; 21956 } 21957 } 21958 21959 for (i = lastParents.length - 1; i >= idx; i--) { 21960 lastCtrl = lastParents[i]; 21961 lastCtrl.fire("mouseleave", { 21962 target : lastCtrl.getEl() 21963 }); 21964 } 21965 } 21966 21967 for (i = idx; i < parents.length; i++) { 21968 ctrl = parents[i]; 21969 ctrl.fire("mouseenter", { 21970 target : ctrl.getEl() 21971 }); 21972 } 21973 } 21974 } 21975 21976 function fixWheelEvent(e) { 21977 e.preventDefault(); 21978 21979 if (e.type == "mousewheel") { 21980 e.deltaY = -1 / 40 * e.wheelDelta; 21981 21982 if (e.wheelDeltaX) { 21983 e.deltaX = -1 / 40 * e.wheelDeltaX; 21984 } 21985 } else { 21986 e.deltaX = 0; 21987 e.deltaY = e.detail; 21988 } 21989 21990 e = self.fire("wheel", e); 21991 } 21992 21993 self._rendered = true; 21994 21995 nativeEvents = self._nativeEvents; 21996 if (nativeEvents) { 21997 // Find event root element if it exists 21998 parents = self.parents().toArray(); 21999 parents.unshift(self); 22000 for (i = 0, l = parents.length; !eventRootCtrl && i < l; i++) { 22001 eventRootCtrl = parents[i]._eventsRoot; 22002 } 22003 22004 // Event root wasn't found the use the root control 22005 if (!eventRootCtrl) { 22006 eventRootCtrl = parents[parents.length - 1] || self; 22007 } 22008 22009 // Set the eventsRoot property on children that didn't have it 22010 self._eventsRoot = eventRootCtrl; 22011 for (l = i, i = 0; i < l; i++) { 22012 parents[i]._eventsRoot = eventRootCtrl; 22013 } 22014 22015 var eventRootDelegates = eventRootCtrl._delegates; 22016 if (!eventRootDelegates) { 22017 eventRootDelegates = eventRootCtrl._delegates = {}; 22018 } 22019 22020 // Bind native event delegates 22021 for (name in nativeEvents) { 22022 if (!nativeEvents) { 22023 return false; 22024 } 22025 22026 if (name === "wheel" && !hasWheelEventSupport) { 22027 if (hasMouseWheelEventSupport) { 22028 DomUtils.on(self.getEl(), "mousewheel", fixWheelEvent); 22029 } else { 22030 DomUtils.on(self.getEl(), "DOMMouseScroll", fixWheelEvent); 22031 } 22032 22033 continue; 22034 } 22035 22036 // Special treatment for mousenter/mouseleave since these doesn't bubble 22037 if (name === "mouseenter" || name === "mouseleave") { 22038 // Fake mousenter/mouseleave 22039 if (!eventRootCtrl._hasMouseEnter) { 22040 DomUtils.on(eventRootCtrl.getEl(), "mouseleave", mouseLeaveHandler); 22041 DomUtils.on(eventRootCtrl.getEl(), "mouseover", mouseEnterHandler); 22042 eventRootCtrl._hasMouseEnter = 1; 22043 } 22044 } else if (!eventRootDelegates[name]) { 22045 DomUtils.on(eventRootCtrl.getEl(), name, delegate); 22046 eventRootDelegates[name] = true; 22047 } 22048 22049 // Remove the event once it's bound 22050 nativeEvents[name] = false; 22051 } 22052 } 22053 }, 22054 22055 getRoot: function() { 22056 var ctrl = this, rootControl, parents = []; 22057 22058 while (ctrl) { 22059 if (ctrl.rootControl) { 22060 rootControl = ctrl.rootControl; 22061 break; 22062 } 22063 22064 parents.push(ctrl); 22065 rootControl = ctrl; 22066 ctrl = ctrl.parent(); 22067 } 22068 22069 if (!rootControl) { 22070 rootControl = this; 22071 } 22072 22073 var i = parents.length; 22074 while (i--) { 22075 parents[i].rootControl = rootControl; 22076 } 22077 22078 return rootControl; 22079 }, 22080 22081 /** 22082 * Reflows the current control and it's parents. 22083 * This should be used after you for example append children to the current control so 22084 * that the layout managers know that they need to reposition everything. 22085 * 22086 * @example 22087 * container.append({type: 'button', text: 'My button'}).reflow(); 22088 * 22089 * @method reflow 22090 * @return {tinymce.ui.Control} Current control instance. 22091 */ 22092 reflow: function() { 22093 this.repaint(); 22094 22095 return this; 22096 } 22097 22098 /** 22099 * Sets/gets the parent container for the control. 22100 * 22101 * @method parent 22102 * @param {tinymce.ui.Container} parent Optional parent to set. 22103 * @return {tinymce.ui.Control} Parent control or the current control on a set action. 22104 */ 22105 // parent: function(parent) {} -- Generated 22106 22107 /** 22108 * Sets/gets the text for the control. 22109 * 22110 * @method text 22111 * @param {String} value Value to set to control. 22112 * @return {String/tinymce.ui.Control} Current control on a set operation or current value on a get. 22113 */ 22114 // text: function(value) {} -- Generated 22115 22116 /** 22117 * Sets/gets the width for the control. 22118 * 22119 * @method width 22120 * @param {Number} value Value to set to control. 22121 * @return {Number/tinymce.ui.Control} Current control on a set operation or current value on a get. 22122 */ 22123 // width: function(value) {} -- Generated 22124 22125 /** 22126 * Sets/gets the height for the control. 22127 * 22128 * @method height 22129 * @param {Number} value Value to set to control. 22130 * @return {Number/tinymce.ui.Control} Current control on a set operation or current value on a get. 22131 */ 22132 // height: function(value) {} -- Generated 22133 22134 /** 22135 * Sets/gets the disabled state on the control. 22136 * 22137 * @method disabled 22138 * @param {Boolean} state Value to set to control. 22139 * @return {Boolean/tinymce.ui.Control} Current control on a set operation or current state on a get. 22140 */ 22141 // disabled: function(state) {} -- Generated 22142 22143 /** 22144 * Sets/gets the active for the control. 22145 * 22146 * @method active 22147 * @param {Boolean} state Value to set to control. 22148 * @return {Boolean/tinymce.ui.Control} Current control on a set operation or current state on a get. 22149 */ 22150 // active: function(state) {} -- Generated 22151 22152 /** 22153 * Sets/gets the name for the control. 22154 * 22155 * @method name 22156 * @param {String} value Value to set to control. 22157 * @return {String/tinymce.ui.Control} Current control on a set operation or current value on a get. 22158 */ 22159 // name: function(value) {} -- Generated 22160 22161 /** 22162 * Sets/gets the title for the control. 22163 * 22164 * @method title 22165 * @param {String} value Value to set to control. 22166 * @return {String/tinymce.ui.Control} Current control on a set operation or current value on a get. 22167 */ 22168 // title: function(value) {} -- Generated 22169 }); 22170 22171 return Control; 22172 }); 22173 22174 // Included from: js/tinymce/classes/ui/Factory.js 22175 22176 /** 22177 * Factory.js 22178 * 22179 * Copyright, Moxiecode Systems AB 22180 * Released under LGPL License. 22181 * 22182 * License: http://www.tinymce.com/license 22183 * Contributing: http://www.tinymce.com/contributing 22184 */ 22185 22186 /*global tinymce:true */ 22187 22188 /** 22189 * This class is a factory for control instances. This enables you 22190 * to create instances of controls without having to require the UI controls directly. 22191 * 22192 * It also allow you to override or add new control types. 22193 * 22194 * @class tinymce.ui.Factory 22195 */ 22196 define("tinymce/ui/Factory", [], function() { 22197 "use strict"; 22198 22199 var types = {}, namespaceInit; 22200 22201 return { 22202 /** 22203 * Adds a new control instance type to the factory. 22204 * 22205 * @method add 22206 * @param {String} type Type name for example "button". 22207 * @param {function} typeClass Class type function. 22208 */ 22209 add: function(type, typeClass) { 22210 types[type.toLowerCase()] = typeClass; 22211 }, 22212 22213 /** 22214 * Returns true/false if the specified type exists or not. 22215 * 22216 * @method has 22217 * @param {String} type Type to look for. 22218 * @return {Boolean} true/false if the control by name exists. 22219 */ 22220 has: function(type) { 22221 return !!types[type.toLowerCase()]; 22222 }, 22223 22224 /** 22225 * Creates a new control instance based on the settings provided. The instance created will be 22226 * based on the specified type property it can also create whole structures of components out of 22227 * the specified JSON object. 22228 * 22229 * @example 22230 * tinymce.ui.Factory.create({ 22231 * type: 'button', 22232 * text: 'Hello world!' 22233 * }); 22234 * 22235 * @method create 22236 * @param {Object/String} settings Name/Value object with items used to create the type. 22237 * @return {tinymce.ui.Control} Control instance based on the specified type. 22238 */ 22239 create: function(type, settings) { 22240 var ControlType, name, namespace; 22241 22242 // Build type lookup 22243 if (!namespaceInit) { 22244 namespace = tinymce.ui; 22245 22246 for (name in namespace) { 22247 types[name.toLowerCase()] = namespace[name]; 22248 } 22249 22250 namespaceInit = true; 22251 } 22252 22253 // If string is specified then use it as the type 22254 if (typeof(type) == 'string') { 22255 settings = settings || {}; 22256 settings.type = type; 22257 } else { 22258 settings = type; 22259 type = settings.type; 22260 } 22261 22262 // Find control type 22263 type = type.toLowerCase(); 22264 ControlType = types[type]; 22265 22266 // #if debug 22267 22268 if (!ControlType) { 22269 throw new Error("Could not find control by type: " + type); 22270 } 22271 22272 // #endif 22273 22274 ControlType = new ControlType(settings); 22275 ControlType.type = type; // Set the type on the instance, this will be used by the Selector engine 22276 22277 return ControlType; 22278 } 22279 }; 22280 }); 22281 22282 // Included from: js/tinymce/classes/ui/KeyboardNavigation.js 22283 22284 /** 22285 * KeyboardNavigation.js 22286 * 22287 * Copyright, Moxiecode Systems AB 22288 * Released under LGPL License. 22289 * 22290 * License: http://www.tinymce.com/license 22291 * Contributing: http://www.tinymce.com/contributing 22292 */ 22293 22294 /** 22295 * This class handles keyboard navigation of controls and elements. 22296 * 22297 * @class tinymce.ui.KeyboardNavigation 22298 */ 22299 define("tinymce/ui/KeyboardNavigation", [ 22300 ], function() { 22301 "use strict"; 22302 22303 /** 22304 * This class handles all keyboard navigation for WAI-ARIA support. Each root container 22305 * gets an instance of this class. 22306 * 22307 * @constructor 22308 */ 22309 return function(settings) { 22310 var root = settings.root, focusedElement, focusedControl; 22311 22312 focusedElement = document.activeElement; 22313 focusedControl = root.getParentCtrl(focusedElement); 22314 22315 /** 22316 * Returns the currently focused elements wai aria role of the currently 22317 * focused element or specified element. 22318 * 22319 * @private 22320 * @param {Element} elm Optional element to get role from. 22321 * @return {String} Role of specified element. 22322 */ 22323 function getRole(elm) { 22324 elm = elm || focusedElement; 22325 22326 return elm && elm.getAttribute('role'); 22327 } 22328 22329 /** 22330 * Returns the wai role of the parent element of the currently 22331 * focused element or specified element. 22332 * 22333 * @private 22334 * @param {Element} elm Optional element to get parent role from. 22335 * @return {String} Role of the first parent that has a role. 22336 */ 22337 function getParentRole(elm) { 22338 var role, parent = elm || focusedElement; 22339 22340 while ((parent = parent.parentNode)) { 22341 if ((role = getRole(parent))) { 22342 return role; 22343 } 22344 } 22345 } 22346 22347 /** 22348 * Returns a wai aria property by name for example aria-selected. 22349 * 22350 * @private 22351 * @param {String} name Name of the aria property to get for example "disabled". 22352 * @return {String} Aria property value. 22353 */ 22354 function getAriaProp(name) { 22355 var elm = focusedElement; 22356 22357 if (elm) { 22358 return elm.getAttribute('aria-' + name); 22359 } 22360 } 22361 22362 /** 22363 * Is the element a text input element or not. 22364 * 22365 * @private 22366 * @param {Element} elm Element to check if it's an text input element or not. 22367 * @return {Boolean} True/false if the element is a text element or not. 22368 */ 22369 function isTextInputElement(elm) { 22370 var tagName = elm.tagName.toUpperCase(); 22371 22372 // Notice: since type can be "email" etc we don't check the type 22373 // So all input elements gets treated as text input elements 22374 return tagName == "INPUT" || tagName == "TEXTAREA"; 22375 } 22376 22377 /** 22378 * Returns true/false if the specified element can be focused or not. 22379 * 22380 * @private 22381 * @param {Element} elm DOM element to check if it can be focused or not. 22382 * @return {Boolean} True/false if the element can have focus. 22383 */ 22384 function canFocus(elm) { 22385 if (isTextInputElement(elm) && !elm.hidden) { 22386 return true; 22387 } 22388 22389 if (/^(button|menuitem|checkbox|tab|menuitemcheckbox|option|gridcell)$/.test(getRole(elm))) { 22390 return true; 22391 } 22392 22393 return false; 22394 } 22395 22396 /** 22397 * Returns an array of focusable visible elements within the specified container element. 22398 * 22399 * @private 22400 * @param {Element} elm DOM element to find focusable elements within. 22401 * @return {Array} Array of focusable elements. 22402 */ 22403 function getFocusElements(elm) { 22404 var elements = []; 22405 22406 function collect(elm) { 22407 if (elm.nodeType != 1 || elm.style.display == 'none') { 22408 return; 22409 } 22410 22411 if (canFocus(elm)) { 22412 elements.push(elm); 22413 } 22414 22415 for (var i = 0; i < elm.childNodes.length; i++) { 22416 collect(elm.childNodes[i]); 22417 } 22418 } 22419 22420 collect(elm || root.getEl()); 22421 22422 return elements; 22423 } 22424 22425 /** 22426 * Returns the navigation root control for the specified control. The navigation root 22427 * is the control that the keyboard navigation gets scoped to for example a menubar or toolbar group. 22428 * It will look for parents of the specified target control or the currenty focused control if this option is omitted. 22429 * 22430 * @private 22431 * @param {tinymce.ui.Control} targetControl Optional target control to find root of. 22432 * @return {tinymce.ui.Control} Navigation root control. 22433 */ 22434 function getNavigationRoot(targetControl) { 22435 var navigationRoot, controls; 22436 22437 targetControl = targetControl || focusedControl; 22438 controls = targetControl.parents().toArray(); 22439 controls.unshift(targetControl); 22440 22441 for (var i = 0; i < controls.length; i++) { 22442 navigationRoot = controls[i]; 22443 22444 if (navigationRoot.settings.ariaRoot) { 22445 break; 22446 } 22447 } 22448 22449 return navigationRoot; 22450 } 22451 22452 /** 22453 * Focuses the first item in the specified targetControl element or the last aria index if the 22454 * navigation root has the ariaRemember option enabled. 22455 * 22456 * @private 22457 * @param {tinymce.ui.Control} targetControl Target control to focus the first item in. 22458 */ 22459 function focusFirst(targetControl) { 22460 var navigationRoot = getNavigationRoot(targetControl); 22461 var focusElements = getFocusElements(navigationRoot.getEl()); 22462 22463 if (navigationRoot.settings.ariaRemember && "lastAriaIndex" in navigationRoot) { 22464 moveFocusToIndex(navigationRoot.lastAriaIndex, focusElements); 22465 } else { 22466 moveFocusToIndex(0, focusElements); 22467 } 22468 } 22469 22470 /** 22471 * Moves the focus to the specified index within the elements list. 22472 * This will scope the index to the size of the element list if it changed. 22473 * 22474 * @private 22475 * @param {Number} idx Specified index to move to. 22476 * @param {Array} elements Array with dom elements to move focus within. 22477 * @return {Number} Input index or a changed index if it was out of range. 22478 */ 22479 function moveFocusToIndex(idx, elements) { 22480 if (idx < 0) { 22481 idx = elements.length - 1; 22482 } else if (idx >= elements.length) { 22483 idx = 0; 22484 } 22485 22486 if (elements[idx]) { 22487 elements[idx].focus(); 22488 } 22489 22490 return idx; 22491 } 22492 22493 /** 22494 * Moves the focus forwards or backwards. 22495 * 22496 * @private 22497 * @param {Number} dir Direction to move in positive means forward, negative means backwards. 22498 * @param {Array} elements Optional array of elements to move within defaults to the current navigation roots elements. 22499 */ 22500 function moveFocus(dir, elements) { 22501 var idx = -1, navigationRoot = getNavigationRoot(); 22502 22503 elements = elements || getFocusElements(navigationRoot.getEl()); 22504 22505 for (var i = 0; i < elements.length; i++) { 22506 if (elements[i] === focusedElement) { 22507 idx = i; 22508 } 22509 } 22510 22511 idx += dir; 22512 navigationRoot.lastAriaIndex = moveFocusToIndex(idx, elements); 22513 } 22514 22515 /** 22516 * Moves the focus to the left this is called by the left key. 22517 * 22518 * @private 22519 */ 22520 function left() { 22521 var parentRole = getParentRole(); 22522 22523 if (parentRole == "tablist") { 22524 moveFocus(-1, getFocusElements(focusedElement.parentNode)); 22525 } else if (focusedControl.parent().submenu) { 22526 cancel(); 22527 } else { 22528 moveFocus(-1); 22529 } 22530 } 22531 22532 /** 22533 * Moves the focus to the right this is called by the right key. 22534 * 22535 * @private 22536 */ 22537 function right() { 22538 var role = getRole(), parentRole = getParentRole(); 22539 22540 if (parentRole == "tablist") { 22541 moveFocus(1, getFocusElements(focusedElement.parentNode)); 22542 } else if (role == "menuitem" && parentRole == "menu" && getAriaProp('haspopup')) { 22543 enter(); 22544 } else { 22545 moveFocus(1); 22546 } 22547 } 22548 22549 /** 22550 * Moves the focus to the up this is called by the up key. 22551 * 22552 * @private 22553 */ 22554 function up() { 22555 moveFocus(-1); 22556 } 22557 22558 /** 22559 * Moves the focus to the up this is called by the down key. 22560 * 22561 * @private 22562 */ 22563 function down() { 22564 var role = getRole(), parentRole = getParentRole(); 22565 22566 if (role == "menuitem" && parentRole == "menubar") { 22567 enter(); 22568 } else if (role == "button" && getAriaProp('haspopup')) { 22569 enter({key: 'down'}); 22570 } else { 22571 moveFocus(1); 22572 } 22573 } 22574 22575 /** 22576 * Moves the focus to the next item or previous item depending on shift key. 22577 * 22578 * @private 22579 * @param {DOMEvent} e DOM event object. 22580 */ 22581 function tab(e) { 22582 var parentRole = getParentRole(); 22583 22584 if (parentRole == "tablist") { 22585 var elm = getFocusElements(focusedControl.getEl('body'))[0]; 22586 22587 if (elm) { 22588 elm.focus(); 22589 } 22590 } else { 22591 moveFocus(e.shiftKey ? -1 : 1); 22592 } 22593 } 22594 22595 /** 22596 * Calls the cancel event on the currently focused control. This is normally done using the Esc key. 22597 * 22598 * @private 22599 */ 22600 function cancel() { 22601 focusedControl.fire('cancel'); 22602 } 22603 22604 /** 22605 * Calls the click event on the currently focused control. This is normally done using the Enter/Space keys. 22606 * 22607 * @private 22608 * @param {Object} aria Optional aria data to pass along with the enter event. 22609 */ 22610 function enter(aria) { 22611 aria = aria || {}; 22612 focusedControl.fire('click', {target: focusedElement, aria: aria}); 22613 } 22614 22615 root.on('keydown', function(e) { 22616 function handleNonTabOrEscEvent(e, handler) { 22617 // Ignore non tab keys for text elements 22618 if (isTextInputElement(focusedElement)) { 22619 return; 22620 } 22621 22622 if (handler(e) !== false) { 22623 e.preventDefault(); 22624 } 22625 } 22626 22627 if (e.isDefaultPrevented()) { 22628 return; 22629 } 22630 22631 switch (e.keyCode) { 22632 case 37: // DOM_VK_LEFT 22633 handleNonTabOrEscEvent(e, left); 22634 break; 22635 22636 case 39: // DOM_VK_RIGHT 22637 handleNonTabOrEscEvent(e, right); 22638 break; 22639 22640 case 38: // DOM_VK_UP 22641 handleNonTabOrEscEvent(e, up); 22642 break; 22643 22644 case 40: // DOM_VK_DOWN 22645 handleNonTabOrEscEvent(e, down); 22646 break; 22647 22648 case 27: // DOM_VK_ESCAPE 22649 cancel(); 22650 break; 22651 22652 case 14: // DOM_VK_ENTER 22653 case 13: // DOM_VK_RETURN 22654 case 32: // DOM_VK_SPACE 22655 handleNonTabOrEscEvent(e, enter); 22656 break; 22657 22658 case 9: // DOM_VK_TAB 22659 if (tab(e) !== false) { 22660 e.preventDefault(); 22661 } 22662 break; 22663 } 22664 }); 22665 22666 root.on('focusin', function(e) { 22667 focusedElement = e.target; 22668 focusedControl = e.control; 22669 }); 22670 22671 return { 22672 focusFirst: focusFirst 22673 }; 22674 }; 22675 }); 22676 22677 // Included from: js/tinymce/classes/ui/Container.js 22678 22679 /** 22680 * Container.js 22681 * 22682 * Copyright, Moxiecode Systems AB 22683 * Released under LGPL License. 22684 * 22685 * License: http://www.tinymce.com/license 22686 * Contributing: http://www.tinymce.com/contributing 22687 */ 22688 22689 /** 22690 * Container control. This is extended by all controls that can have 22691 * children such as panels etc. You can also use this class directly as an 22692 * generic container instance. The container doesn't have any specific role or style. 22693 * 22694 * @-x-less Container.less 22695 * @class tinymce.ui.Container 22696 * @extends tinymce.ui.Control 22697 */ 22698 define("tinymce/ui/Container", [ 22699 "tinymce/ui/Control", 22700 "tinymce/ui/Collection", 22701 "tinymce/ui/Selector", 22702 "tinymce/ui/Factory", 22703 "tinymce/ui/KeyboardNavigation", 22704 "tinymce/util/Tools", 22705 "tinymce/ui/DomUtils" 22706 ], function(Control, Collection, Selector, Factory, KeyboardNavigation, Tools, DomUtils) { 22707 "use strict"; 22708 22709 var selectorCache = {}; 22710 22711 return Control.extend({ 22712 layout: '', 22713 innerClass: 'container-inner', 22714 22715 /** 22716 * Constructs a new control instance with the specified settings. 22717 * 22718 * @constructor 22719 * @param {Object} settings Name/value object with settings. 22720 * @setting {Array} items Items to add to container in JSON format or control instances. 22721 * @setting {String} layout Layout manager by name to use. 22722 * @setting {Object} defaults Default settings to apply to all items. 22723 */ 22724 init: function(settings) { 22725 var self = this; 22726 22727 self._super(settings); 22728 settings = self.settings; 22729 self._fixed = settings.fixed; 22730 self._items = new Collection(); 22731 22732 if (self.isRtl()) { 22733 self.addClass('rtl'); 22734 } 22735 22736 self.addClass('container'); 22737 self.addClass('container-body', 'body'); 22738 22739 if (settings.containerCls) { 22740 self.addClass(settings.containerCls); 22741 } 22742 22743 self._layout = Factory.create((settings.layout || self.layout) + 'layout'); 22744 22745 if (self.settings.items) { 22746 self.add(self.settings.items); 22747 } 22748 22749 // TODO: Fix this! 22750 self._hasBody = true; 22751 }, 22752 22753 /** 22754 * Returns a collection of child items that the container currently have. 22755 * 22756 * @method items 22757 * @return {tinymce.ui.Collection} Control collection direct child controls. 22758 */ 22759 items: function() { 22760 return this._items; 22761 }, 22762 22763 /** 22764 * Find child controls by selector. 22765 * 22766 * @method find 22767 * @param {String} selector Selector CSS pattern to find children by. 22768 * @return {tinymce.ui.Collection} Control collection with child controls. 22769 */ 22770 find: function(selector) { 22771 selector = selectorCache[selector] = selectorCache[selector] || new Selector(selector); 22772 22773 return selector.find(this); 22774 }, 22775 22776 /** 22777 * Adds one or many items to the current container. This will create instances of 22778 * the object representations if needed. 22779 * 22780 * @method add 22781 * @param {Array/Object/tinymce.ui.Control} items Array or item that will be added to the container. 22782 * @return {tinymce.ui.Collection} Current collection control. 22783 */ 22784 add: function(items) { 22785 var self = this; 22786 22787 self.items().add(self.create(items)).parent(self); 22788 22789 return self; 22790 }, 22791 22792 /** 22793 * Focuses the current container instance. This will look 22794 * for the first control in the container and focus that. 22795 * 22796 * @method focus 22797 * @param {Boolean} keyboard Optional true/false if the focus was a keyboard focus or not. 22798 * @return {tinymce.ui.Collection} Current instance. 22799 */ 22800 focus: function(keyboard) { 22801 var self = this, focusCtrl, keyboardNav, items; 22802 22803 if (keyboard) { 22804 keyboardNav = self.keyboardNav || self.parents().eq(-1)[0].keyboardNav; 22805 22806 if (keyboardNav) { 22807 keyboardNav.focusFirst(self); 22808 return; 22809 } 22810 } 22811 22812 items = self.find('*'); 22813 22814 // TODO: Figure out a better way to auto focus alert dialog buttons 22815 if (self.statusbar) { 22816 items.add(self.statusbar.items()); 22817 } 22818 22819 items.each(function(ctrl) { 22820 if (ctrl.settings.autofocus) { 22821 focusCtrl = null; 22822 return false; 22823 } 22824 22825 if (ctrl.canFocus) { 22826 focusCtrl = focusCtrl || ctrl; 22827 } 22828 }); 22829 22830 if (focusCtrl) { 22831 focusCtrl.focus(); 22832 } 22833 22834 return self; 22835 }, 22836 22837 /** 22838 * Replaces the specified child control with a new control. 22839 * 22840 * @method replace 22841 * @param {tinymce.ui.Control} oldItem Old item to be replaced. 22842 * @param {tinymce.ui.Control} newItem New item to be inserted. 22843 */ 22844 replace: function(oldItem, newItem) { 22845 var ctrlElm, items = this.items(), i = items.length; 22846 22847 // Replace the item in collection 22848 while (i--) { 22849 if (items[i] === oldItem) { 22850 items[i] = newItem; 22851 break; 22852 } 22853 } 22854 22855 if (i >= 0) { 22856 // Remove new item from DOM 22857 ctrlElm = newItem.getEl(); 22858 if (ctrlElm) { 22859 ctrlElm.parentNode.removeChild(ctrlElm); 22860 } 22861 22862 // Remove old item from DOM 22863 ctrlElm = oldItem.getEl(); 22864 if (ctrlElm) { 22865 ctrlElm.parentNode.removeChild(ctrlElm); 22866 } 22867 } 22868 22869 // Adopt the item 22870 newItem.parent(this); 22871 }, 22872 22873 /** 22874 * Creates the specified items. If any of the items is plain JSON style objects 22875 * it will convert these into real tinymce.ui.Control instances. 22876 * 22877 * @method create 22878 * @param {Array} items Array of items to convert into control instances. 22879 * @return {Array} Array with control instances. 22880 */ 22881 create: function(items) { 22882 var self = this, settings, ctrlItems = []; 22883 22884 // Non array structure, then force it into an array 22885 if (!Tools.isArray(items)) { 22886 items = [items]; 22887 } 22888 22889 // Add default type to each child control 22890 Tools.each(items, function(item) { 22891 if (item) { 22892 // Construct item if needed 22893 if (!(item instanceof Control)) { 22894 // Name only then convert it to an object 22895 if (typeof(item) == "string") { 22896 item = {type: item}; 22897 } 22898 22899 // Create control instance based on input settings and default settings 22900 settings = Tools.extend({}, self.settings.defaults, item); 22901 item.type = settings.type = settings.type || item.type || self.settings.defaultType || 22902 (settings.defaults ? settings.defaults.type : null); 22903 item = Factory.create(settings); 22904 } 22905 22906 ctrlItems.push(item); 22907 } 22908 }); 22909 22910 return ctrlItems; 22911 }, 22912 22913 /** 22914 * Renders new control instances. 22915 * 22916 * @private 22917 */ 22918 renderNew: function() { 22919 var self = this; 22920 22921 // Render any new items 22922 self.items().each(function(ctrl, index) { 22923 var containerElm, fragment; 22924 22925 ctrl.parent(self); 22926 22927 if (!ctrl._rendered) { 22928 containerElm = self.getEl('body'); 22929 fragment = DomUtils.createFragment(ctrl.renderHtml()); 22930 22931 // Insert or append the item 22932 if (containerElm.hasChildNodes() && index <= containerElm.childNodes.length - 1) { 22933 containerElm.insertBefore(fragment, containerElm.childNodes[index]); 22934 } else { 22935 containerElm.appendChild(fragment); 22936 } 22937 22938 ctrl.postRender(); 22939 } 22940 }); 22941 22942 self._layout.applyClasses(self); 22943 self._lastRect = null; 22944 22945 return self; 22946 }, 22947 22948 /** 22949 * Appends new instances to the current container. 22950 * 22951 * @method append 22952 * @param {Array/tinymce.ui.Collection} items Array if controls to append. 22953 * @return {tinymce.ui.Container} Current container instance. 22954 */ 22955 append: function(items) { 22956 return this.add(items).renderNew(); 22957 }, 22958 22959 /** 22960 * Prepends new instances to the current container. 22961 * 22962 * @method prepend 22963 * @param {Array/tinymce.ui.Collection} items Array if controls to prepend. 22964 * @return {tinymce.ui.Container} Current container instance. 22965 */ 22966 prepend: function(items) { 22967 var self = this; 22968 22969 self.items().set(self.create(items).concat(self.items().toArray())); 22970 22971 return self.renderNew(); 22972 }, 22973 22974 /** 22975 * Inserts an control at a specific index. 22976 * 22977 * @method insert 22978 * @param {Array/tinymce.ui.Collection} items Array if controls to insert. 22979 * @param {Number} index Index to insert controls at. 22980 * @param {Boolean} [before=false] Inserts controls before the index. 22981 */ 22982 insert: function(items, index, before) { 22983 var self = this, curItems, beforeItems, afterItems; 22984 22985 items = self.create(items); 22986 curItems = self.items(); 22987 22988 if (!before && index < curItems.length - 1) { 22989 index += 1; 22990 } 22991 22992 if (index >= 0 && index < curItems.length) { 22993 beforeItems = curItems.slice(0, index).toArray(); 22994 afterItems = curItems.slice(index).toArray(); 22995 curItems.set(beforeItems.concat(items, afterItems)); 22996 } 22997 22998 return self.renderNew(); 22999 }, 23000 23001 /** 23002 * Populates the form fields from the specified JSON data object. 23003 * 23004 * Control items in the form that matches the data will have it's value set. 23005 * 23006 * @method fromJSON 23007 * @param {Object} data JSON data object to set control values by. 23008 * @return {tinymce.ui.Container} Current form instance. 23009 */ 23010 fromJSON: function(data) { 23011 var self = this; 23012 23013 for (var name in data) { 23014 self.find('#' + name).value(data[name]); 23015 } 23016 23017 return self; 23018 }, 23019 23020 /** 23021 * Serializes the form into a JSON object by getting all items 23022 * that has a name and a value. 23023 * 23024 * @method toJSON 23025 * @return {Object} JSON object with form data. 23026 */ 23027 toJSON: function() { 23028 var self = this, data = {}; 23029 23030 self.find('*').each(function(ctrl) { 23031 var name = ctrl.name(), value = ctrl.value(); 23032 23033 if (name && typeof(value) != "undefined") { 23034 data[name] = value; 23035 } 23036 }); 23037 23038 return data; 23039 }, 23040 23041 preRender: function() { 23042 }, 23043 23044 /** 23045 * Renders the control as a HTML string. 23046 * 23047 * @method renderHtml 23048 * @return {String} HTML representing the control. 23049 */ 23050 renderHtml: function() { 23051 var self = this, layout = self._layout, role = this.settings.role; 23052 23053 self.preRender(); 23054 layout.preRender(self); 23055 23056 return ( 23057 '<div id="' + self._id + '" class="' + self.classes() + '"' + (role ? ' role="' + this.settings.role + '"' : '') + '>' + 23058 '<div id="' + self._id + '-body" class="' + self.classes('body') + '">' + 23059 (self.settings.html || '') + layout.renderHtml(self) + 23060 '</div>' + 23061 '</div>' 23062 ); 23063 }, 23064 23065 /** 23066 * Post render method. Called after the control has been rendered to the target. 23067 * 23068 * @method postRender 23069 * @return {tinymce.ui.Container} Current combobox instance. 23070 */ 23071 postRender: function() { 23072 var self = this, box; 23073 23074 self.items().exec('postRender'); 23075 self._super(); 23076 23077 self._layout.postRender(self); 23078 self._rendered = true; 23079 23080 if (self.settings.style) { 23081 DomUtils.css(self.getEl(), self.settings.style); 23082 } 23083 23084 if (self.settings.border) { 23085 box = self.borderBox(); 23086 DomUtils.css(self.getEl(), { 23087 'border-top-width': box.top, 23088 'border-right-width': box.right, 23089 'border-bottom-width': box.bottom, 23090 'border-left-width': box.left 23091 }); 23092 } 23093 23094 if (!self.parent()) { 23095 self.keyboardNav = new KeyboardNavigation({ 23096 root: self 23097 }); 23098 } 23099 23100 return self; 23101 }, 23102 23103 /** 23104 * Initializes the current controls layout rect. 23105 * This will be executed by the layout managers to determine the 23106 * default minWidth/minHeight etc. 23107 * 23108 * @method initLayoutRect 23109 * @return {Object} Layout rect instance. 23110 */ 23111 initLayoutRect: function() { 23112 var self = this, layoutRect = self._super(); 23113 23114 // Recalc container size by asking layout manager 23115 self._layout.recalc(self); 23116 23117 return layoutRect; 23118 }, 23119 23120 /** 23121 * Recalculates the positions of the controls in the current container. 23122 * This is invoked by the reflow method and shouldn't be called directly. 23123 * 23124 * @method recalc 23125 */ 23126 recalc: function() { 23127 var self = this, rect = self._layoutRect, lastRect = self._lastRect; 23128 23129 if (!lastRect || lastRect.w != rect.w || lastRect.h != rect.h) { 23130 self._layout.recalc(self); 23131 rect = self.layoutRect(); 23132 self._lastRect = {x: rect.x, y: rect.y, w: rect.w, h: rect.h}; 23133 return true; 23134 } 23135 }, 23136 23137 /** 23138 * Reflows the current container and it's children and possible parents. 23139 * This should be used after you for example append children to the current control so 23140 * that the layout managers know that they need to reposition everything. 23141 * 23142 * @example 23143 * container.append({type: 'button', text: 'My button'}).reflow(); 23144 * 23145 * @method reflow 23146 * @return {tinymce.ui.Container} Current container instance. 23147 */ 23148 reflow: function() { 23149 var i; 23150 23151 if (this.visible()) { 23152 Control.repaintControls = []; 23153 Control.repaintControls.map = {}; 23154 23155 this.recalc(); 23156 i = Control.repaintControls.length; 23157 23158 while (i--) { 23159 Control.repaintControls[i].repaint(); 23160 } 23161 23162 // TODO: Fix me! 23163 if (this.settings.layout !== "flow" && this.settings.layout !== "stack") { 23164 this.repaint(); 23165 } 23166 23167 Control.repaintControls = []; 23168 } 23169 23170 return this; 23171 } 23172 }); 23173 }); 23174 23175 // Included from: js/tinymce/classes/ui/DragHelper.js 23176 23177 /** 23178 * DragHelper.js 23179 * 23180 * Copyright, Moxiecode Systems AB 23181 * Released under LGPL License. 23182 * 23183 * License: http://www.tinymce.com/license 23184 * Contributing: http://www.tinymce.com/contributing 23185 */ 23186 23187 /** 23188 * Drag/drop helper class. 23189 * 23190 * @example 23191 * var dragHelper = new tinymce.ui.DragHelper('mydiv', { 23192 * start: function(evt) { 23193 * }, 23194 * 23195 * drag: function(evt) { 23196 * }, 23197 * 23198 * end: function(evt) { 23199 * } 23200 * }); 23201 * 23202 * @class tinymce.ui.DragHelper 23203 */ 23204 define("tinymce/ui/DragHelper", [ 23205 "tinymce/ui/DomUtils" 23206 ], function(DomUtils) { 23207 "use strict"; 23208 23209 function getDocumentSize() { 23210 var doc = document, documentElement, body, scrollWidth, clientWidth; 23211 var offsetWidth, scrollHeight, clientHeight, offsetHeight, max = Math.max; 23212 23213 documentElement = doc.documentElement; 23214 body = doc.body; 23215 23216 scrollWidth = max(documentElement.scrollWidth, body.scrollWidth); 23217 clientWidth = max(documentElement.clientWidth, body.clientWidth); 23218 offsetWidth = max(documentElement.offsetWidth, body.offsetWidth); 23219 23220 scrollHeight = max(documentElement.scrollHeight, body.scrollHeight); 23221 clientHeight = max(documentElement.clientHeight, body.clientHeight); 23222 offsetHeight = max(documentElement.offsetHeight, body.offsetHeight); 23223 23224 return { 23225 width: scrollWidth < offsetWidth ? clientWidth : scrollWidth, 23226 height: scrollHeight < offsetHeight ? clientHeight : scrollHeight 23227 }; 23228 } 23229 23230 return function(id, settings) { 23231 var eventOverlayElm, doc = document, downButton, start, stop, drag, startX, startY; 23232 23233 settings = settings || {}; 23234 23235 function getHandleElm() { 23236 return doc.getElementById(settings.handle || id); 23237 } 23238 23239 start = function(e) { 23240 var docSize = getDocumentSize(), handleElm, cursor; 23241 23242 e.preventDefault(); 23243 downButton = e.button; 23244 handleElm = getHandleElm(); 23245 startX = e.screenX; 23246 startY = e.screenY; 23247 23248 // Grab cursor from handle 23249 if (window.getComputedStyle) { 23250 cursor = window.getComputedStyle(handleElm, null).getPropertyValue("cursor"); 23251 } else { 23252 cursor = handleElm.runtimeStyle.cursor; 23253 } 23254 23255 // Create event overlay and add it to document 23256 eventOverlayElm = doc.createElement('div'); 23257 DomUtils.css(eventOverlayElm, { 23258 position: "absolute", 23259 top: 0, left: 0, 23260 width: docSize.width, 23261 height: docSize.height, 23262 zIndex: 0x7FFFFFFF, 23263 opacity: 0.0001, 23264 background: 'red', 23265 cursor: cursor 23266 }); 23267 23268 doc.body.appendChild(eventOverlayElm); 23269 23270 // Bind mouse events 23271 DomUtils.on(doc, 'mousemove', drag); 23272 DomUtils.on(doc, 'mouseup', stop); 23273 23274 // Begin drag 23275 settings.start(e); 23276 }; 23277 23278 drag = function(e) { 23279 if (e.button !== downButton) { 23280 return stop(e); 23281 } 23282 23283 e.deltaX = e.screenX - startX; 23284 e.deltaY = e.screenY - startY; 23285 23286 e.preventDefault(); 23287 settings.drag(e); 23288 }; 23289 23290 stop = function(e) { 23291 DomUtils.off(doc, 'mousemove', drag); 23292 DomUtils.off(doc, 'mouseup', stop); 23293 23294 eventOverlayElm.parentNode.removeChild(eventOverlayElm); 23295 23296 if (settings.stop) { 23297 settings.stop(e); 23298 } 23299 }; 23300 23301 /** 23302 * Destroys the drag/drop helper instance. 23303 * 23304 * @method destroy 23305 */ 23306 this.destroy = function() { 23307 DomUtils.off(getHandleElm()); 23308 }; 23309 23310 DomUtils.on(getHandleElm(), 'mousedown', start); 23311 }; 23312 }); 23313 23314 // Included from: js/tinymce/classes/ui/Scrollable.js 23315 23316 /** 23317 * Scrollable.js 23318 * 23319 * Copyright, Moxiecode Systems AB 23320 * Released under LGPL License. 23321 * 23322 * License: http://www.tinymce.com/license 23323 * Contributing: http://www.tinymce.com/contributing 23324 */ 23325 23326 /** 23327 * This mixin makes controls scrollable using custom scrollbars. 23328 * 23329 * @-x-less Scrollable.less 23330 * @mixin tinymce.ui.Scrollable 23331 */ 23332 define("tinymce/ui/Scrollable", [ 23333 "tinymce/ui/DomUtils", 23334 "tinymce/ui/DragHelper" 23335 ], function(DomUtils, DragHelper) { 23336 "use strict"; 23337 23338 return { 23339 init: function() { 23340 var self = this; 23341 self.on('repaint', self.renderScroll); 23342 }, 23343 23344 renderScroll: function() { 23345 var self = this, margin = 2; 23346 23347 function repaintScroll() { 23348 var hasScrollH, hasScrollV, bodyElm; 23349 23350 function repaintAxis(axisName, posName, sizeName, contentSizeName, hasScroll, ax) { 23351 var containerElm, scrollBarElm, scrollThumbElm; 23352 var containerSize, scrollSize, ratio, rect; 23353 var posNameLower, sizeNameLower; 23354 23355 scrollBarElm = self.getEl('scroll' + axisName); 23356 if (scrollBarElm) { 23357 posNameLower = posName.toLowerCase(); 23358 sizeNameLower = sizeName.toLowerCase(); 23359 23360 if (self.getEl('absend')) { 23361 DomUtils.css(self.getEl('absend'), posNameLower, self.layoutRect()[contentSizeName] - 1); 23362 } 23363 23364 if (!hasScroll) { 23365 DomUtils.css(scrollBarElm, 'display', 'none'); 23366 return; 23367 } 23368 23369 DomUtils.css(scrollBarElm, 'display', 'block'); 23370 containerElm = self.getEl('body'); 23371 scrollThumbElm = self.getEl('scroll' + axisName + "t"); 23372 containerSize = containerElm["client" + sizeName] - (margin * 2); 23373 containerSize -= hasScrollH && hasScrollV ? scrollBarElm["client" + ax] : 0; 23374 scrollSize = containerElm["scroll" + sizeName]; 23375 ratio = containerSize / scrollSize; 23376 23377 rect = {}; 23378 rect[posNameLower] = containerElm["offset" + posName] + margin; 23379 rect[sizeNameLower] = containerSize; 23380 DomUtils.css(scrollBarElm, rect); 23381 23382 rect = {}; 23383 rect[posNameLower] = containerElm["scroll" + posName] * ratio; 23384 rect[sizeNameLower] = containerSize * ratio; 23385 DomUtils.css(scrollThumbElm, rect); 23386 } 23387 } 23388 23389 bodyElm = self.getEl('body'); 23390 hasScrollH = bodyElm.scrollWidth > bodyElm.clientWidth; 23391 hasScrollV = bodyElm.scrollHeight > bodyElm.clientHeight; 23392 23393 repaintAxis("h", "Left", "Width", "contentW", hasScrollH, "Height"); 23394 repaintAxis("v", "Top", "Height", "contentH", hasScrollV, "Width"); 23395 } 23396 23397 function addScroll() { 23398 function addScrollAxis(axisName, posName, sizeName, deltaPosName, ax) { 23399 var scrollStart, axisId = self._id + '-scroll' + axisName, prefix = self.classPrefix; 23400 23401 self.getEl().appendChild(DomUtils.createFragment( 23402 '<div id="' + axisId + '" class="' + prefix + 'scrollbar ' + prefix + 'scrollbar-' + axisName + '">' + 23403 '<div id="' + axisId + 't" class="' + prefix + 'scrollbar-thumb"></div>' + 23404 '</div>' 23405 )); 23406 23407 self.draghelper = new DragHelper(axisId + 't', { 23408 start: function() { 23409 scrollStart = self.getEl('body')["scroll" + posName]; 23410 DomUtils.addClass(DomUtils.get(axisId), prefix + 'active'); 23411 }, 23412 23413 drag: function(e) { 23414 var ratio, hasScrollH, hasScrollV, containerSize, layoutRect = self.layoutRect(); 23415 23416 hasScrollH = layoutRect.contentW > layoutRect.innerW; 23417 hasScrollV = layoutRect.contentH > layoutRect.innerH; 23418 containerSize = self.getEl('body')["client" + sizeName] - (margin * 2); 23419 containerSize -= hasScrollH && hasScrollV ? self.getEl('scroll' + axisName)["client" + ax] : 0; 23420 23421 ratio = containerSize / self.getEl('body')["scroll" + sizeName]; 23422 self.getEl('body')["scroll" + posName] = scrollStart + (e["delta" + deltaPosName] / ratio); 23423 }, 23424 23425 stop: function() { 23426 DomUtils.removeClass(DomUtils.get(axisId), prefix + 'active'); 23427 } 23428 }); 23429 /* 23430 self.on('click', function(e) { 23431 if (e.target.id == self._id + '-scrollv') { 23432 23433 } 23434 });*/ 23435 } 23436 23437 self.addClass('scroll'); 23438 23439 addScrollAxis("v", "Top", "Height", "Y", "Width"); 23440 addScrollAxis("h", "Left", "Width", "X", "Height"); 23441 } 23442 23443 if (self.settings.autoScroll) { 23444 if (!self._hasScroll) { 23445 self._hasScroll = true; 23446 addScroll(); 23447 23448 self.on('wheel', function(e) { 23449 var bodyEl = self.getEl('body'); 23450 23451 bodyEl.scrollLeft += (e.deltaX || 0) * 10; 23452 bodyEl.scrollTop += e.deltaY * 10; 23453 23454 repaintScroll(); 23455 }); 23456 23457 DomUtils.on(self.getEl('body'), "scroll", repaintScroll); 23458 } 23459 23460 repaintScroll(); 23461 } 23462 } 23463 }; 23464 }); 23465 23466 // Included from: js/tinymce/classes/ui/Panel.js 23467 23468 /** 23469 * Panel.js 23470 * 23471 * Copyright, Moxiecode Systems AB 23472 * Released under LGPL License. 23473 * 23474 * License: http://www.tinymce.com/license 23475 * Contributing: http://www.tinymce.com/contributing 23476 */ 23477 23478 /** 23479 * Creates a new panel. 23480 * 23481 * @-x-less Panel.less 23482 * @class tinymce.ui.Panel 23483 * @extends tinymce.ui.Container 23484 * @mixes tinymce.ui.Scrollable 23485 */ 23486 define("tinymce/ui/Panel", [ 23487 "tinymce/ui/Container", 23488 "tinymce/ui/Scrollable" 23489 ], function(Container, Scrollable) { 23490 "use strict"; 23491 23492 return Container.extend({ 23493 Defaults: { 23494 layout: 'fit', 23495 containerCls: 'panel' 23496 }, 23497 23498 Mixins: [Scrollable], 23499 23500 /** 23501 * Renders the control as a HTML string. 23502 * 23503 * @method renderHtml 23504 * @return {String} HTML representing the control. 23505 */ 23506 renderHtml: function() { 23507 var self = this, layout = self._layout, innerHtml = self.settings.html; 23508 23509 self.preRender(); 23510 layout.preRender(self); 23511 23512 if (typeof(innerHtml) == "undefined") { 23513 innerHtml = ( 23514 '<div id="' + self._id + '-body" class="' + self.classes('body') + '">' + 23515 layout.renderHtml(self) + 23516 '</div>' 23517 ); 23518 } else { 23519 if (typeof(innerHtml) == 'function') { 23520 innerHtml = innerHtml.call(self); 23521 } 23522 23523 self._hasBody = false; 23524 } 23525 23526 return ( 23527 '<div id="' + self._id + '" class="' + self.classes() + '" hidefocus="1" tabindex="-1" role="group">' + 23528 (self._preBodyHtml || '') + 23529 innerHtml + 23530 '</div>' 23531 ); 23532 } 23533 }); 23534 }); 23535 23536 // Included from: js/tinymce/classes/ui/Movable.js 23537 23538 /** 23539 * Movable.js 23540 * 23541 * Copyright, Moxiecode Systems AB 23542 * Released under LGPL License. 23543 * 23544 * License: http://www.tinymce.com/license 23545 * Contributing: http://www.tinymce.com/contributing 23546 */ 23547 23548 /** 23549 * Movable mixin. Makes controls movable absolute and relative to other elements. 23550 * 23551 * @mixin tinymce.ui.Movable 23552 */ 23553 define("tinymce/ui/Movable", [ 23554 "tinymce/ui/DomUtils" 23555 ], function(DomUtils) { 23556 "use strict"; 23557 23558 function calculateRelativePosition(ctrl, targetElm, rel) { 23559 var ctrlElm, pos, x, y, selfW, selfH, targetW, targetH, viewport, size; 23560 23561 viewport = DomUtils.getViewPort(); 23562 23563 // Get pos of target 23564 pos = DomUtils.getPos(targetElm); 23565 x = pos.x; 23566 y = pos.y; 23567 23568 if (ctrl._fixed) { 23569 x -= viewport.x; 23570 y -= viewport.y; 23571 } 23572 23573 // Get size of self 23574 ctrlElm = ctrl.getEl(); 23575 size = DomUtils.getSize(ctrlElm); 23576 selfW = size.width; 23577 selfH = size.height; 23578 23579 // Get size of target 23580 size = DomUtils.getSize(targetElm); 23581 targetW = size.width; 23582 targetH = size.height; 23583 23584 // Parse align string 23585 rel = (rel || '').split(''); 23586 23587 // Target corners 23588 if (rel[0] === 'b') { 23589 y += targetH; 23590 } 23591 23592 if (rel[1] === 'r') { 23593 x += targetW; 23594 } 23595 23596 if (rel[0] === 'c') { 23597 y += Math.round(targetH / 2); 23598 } 23599 23600 if (rel[1] === 'c') { 23601 x += Math.round(targetW / 2); 23602 } 23603 23604 // Self corners 23605 if (rel[3] === 'b') { 23606 y -= selfH; 23607 } 23608 23609 if (rel[4] === 'r') { 23610 x -= selfW; 23611 } 23612 23613 if (rel[3] === 'c') { 23614 y -= Math.round(selfH / 2); 23615 } 23616 23617 if (rel[4] === 'c') { 23618 x -= Math.round(selfW / 2); 23619 } 23620 23621 return { 23622 x: x, 23623 y: y, 23624 w: selfW, 23625 h: selfH 23626 }; 23627 } 23628 23629 return { 23630 /** 23631 * Tests various positions to get the most suitable one. 23632 * 23633 * @method testMoveRel 23634 * @param {DOMElement} elm Element to position against. 23635 * @param {Array} rels Array with relative positions. 23636 * @return {String} Best suitable relative position. 23637 */ 23638 testMoveRel: function(elm, rels) { 23639 var viewPortRect = DomUtils.getViewPort(); 23640 23641 for (var i = 0; i < rels.length; i++) { 23642 var pos = calculateRelativePosition(this, elm, rels[i]); 23643 23644 if (this._fixed) { 23645 if (pos.x > 0 && pos.x + pos.w < viewPortRect.w && pos.y > 0 && pos.y + pos.h < viewPortRect.h) { 23646 return rels[i]; 23647 } 23648 } else { 23649 if (pos.x > viewPortRect.x && pos.x + pos.w < viewPortRect.w + viewPortRect.x && 23650 pos.y > viewPortRect.y && pos.y + pos.h < viewPortRect.h + viewPortRect.y) { 23651 return rels[i]; 23652 } 23653 } 23654 } 23655 23656 return rels[0]; 23657 }, 23658 23659 /** 23660 * Move relative to the specified element. 23661 * 23662 * @method moveRel 23663 * @param {Element} elm Element to move relative to. 23664 * @param {String} rel Relative mode. For example: br-tl. 23665 * @return {tinymce.ui.Control} Current control instance. 23666 */ 23667 moveRel: function(elm, rel) { 23668 if (typeof(rel) != 'string') { 23669 rel = this.testMoveRel(elm, rel); 23670 } 23671 23672 var pos = calculateRelativePosition(this, elm, rel); 23673 return this.moveTo(pos.x, pos.y); 23674 }, 23675 23676 /** 23677 * Move by a relative x, y values. 23678 * 23679 * @method moveBy 23680 * @param {Number} dx Relative x position. 23681 * @param {Number} dy Relative y position. 23682 * @return {tinymce.ui.Control} Current control instance. 23683 */ 23684 moveBy: function(dx, dy) { 23685 var self = this, rect = self.layoutRect(); 23686 23687 self.moveTo(rect.x + dx, rect.y + dy); 23688 23689 return self; 23690 }, 23691 23692 /** 23693 * Move to absolute position. 23694 * 23695 * @method moveTo 23696 * @param {Number} x Absolute x position. 23697 * @param {Number} y Absolute y position. 23698 * @return {tinymce.ui.Control} Current control instance. 23699 */ 23700 moveTo: function(x, y) { 23701 var self = this; 23702 23703 // TODO: Move this to some global class 23704 function contrain(value, max, size) { 23705 if (value < 0) { 23706 return 0; 23707 } 23708 23709 if (value + size > max) { 23710 value = max - size; 23711 return value < 0 ? 0 : value; 23712 } 23713 23714 return value; 23715 } 23716 23717 if (self.settings.constrainToViewport) { 23718 var viewPortRect = DomUtils.getViewPort(window); 23719 var layoutRect = self.layoutRect(); 23720 23721 x = contrain(x, viewPortRect.w + viewPortRect.x, layoutRect.w); 23722 y = contrain(y, viewPortRect.h + viewPortRect.y, layoutRect.h); 23723 } 23724 23725 if (self._rendered) { 23726 self.layoutRect({x: x, y: y}).repaint(); 23727 } else { 23728 self.settings.x = x; 23729 self.settings.y = y; 23730 } 23731 23732 self.fire('move', {x: x, y: y}); 23733 23734 return self; 23735 } 23736 }; 23737 }); 23738 23739 // Included from: js/tinymce/classes/ui/Resizable.js 23740 23741 /** 23742 * Resizable.js 23743 * 23744 * Copyright, Moxiecode Systems AB 23745 * Released under LGPL License. 23746 * 23747 * License: http://www.tinymce.com/license 23748 * Contributing: http://www.tinymce.com/contributing 23749 */ 23750 23751 /** 23752 * Resizable mixin. Enables controls to be resized. 23753 * 23754 * @mixin tinymce.ui.Resizable 23755 */ 23756 define("tinymce/ui/Resizable", [ 23757 "tinymce/ui/DomUtils" 23758 ], function(DomUtils) { 23759 "use strict"; 23760 23761 return { 23762 /** 23763 * Resizes the control to contents. 23764 * 23765 * @method resizeToContent 23766 */ 23767 resizeToContent: function() { 23768 this._layoutRect.autoResize = true; 23769 this._lastRect = null; 23770 this.reflow(); 23771 }, 23772 23773 /** 23774 * Resizes the control to a specific width/height. 23775 * 23776 * @method resizeTo 23777 * @param {Number} w Control width. 23778 * @param {Number} h Control height. 23779 * @return {tinymce.ui.Control} Current control instance. 23780 */ 23781 resizeTo: function(w, h) { 23782 // TODO: Fix hack 23783 if (w <= 1 || h <= 1) { 23784 var rect = DomUtils.getWindowSize(); 23785 23786 w = w <= 1 ? w * rect.w : w; 23787 h = h <= 1 ? h * rect.h : h; 23788 } 23789 23790 this._layoutRect.autoResize = false; 23791 return this.layoutRect({minW: w, minH: h, w: w, h: h}).reflow(); 23792 }, 23793 23794 /** 23795 * Resizes the control to a specific relative width/height. 23796 * 23797 * @method resizeBy 23798 * @param {Number} dw Relative control width. 23799 * @param {Number} dh Relative control height. 23800 * @return {tinymce.ui.Control} Current control instance. 23801 */ 23802 resizeBy: function(dw, dh) { 23803 var self = this, rect = self.layoutRect(); 23804 23805 return self.resizeTo(rect.w + dw, rect.h + dh); 23806 } 23807 }; 23808 }); 23809 23810 // Included from: js/tinymce/classes/ui/FloatPanel.js 23811 23812 /** 23813 * FloatPanel.js 23814 * 23815 * Copyright, Moxiecode Systems AB 23816 * Released under LGPL License. 23817 * 23818 * License: http://www.tinymce.com/license 23819 * Contributing: http://www.tinymce.com/contributing 23820 */ 23821 23822 /** 23823 * This class creates a floating panel. 23824 * 23825 * @-x-less FloatPanel.less 23826 * @class tinymce.ui.FloatPanel 23827 * @extends tinymce.ui.Panel 23828 * @mixes tinymce.ui.Movable 23829 * @mixes tinymce.ui.Resizable 23830 */ 23831 define("tinymce/ui/FloatPanel", [ 23832 "tinymce/ui/Panel", 23833 "tinymce/ui/Movable", 23834 "tinymce/ui/Resizable", 23835 "tinymce/ui/DomUtils" 23836 ], function(Panel, Movable, Resizable, DomUtils) { 23837 "use strict"; 23838 23839 var documentClickHandler, documentScrollHandler, visiblePanels = []; 23840 var zOrder = [], hasModal; 23841 23842 var FloatPanel = Panel.extend({ 23843 Mixins: [Movable, Resizable], 23844 23845 /** 23846 * Constructs a new control instance with the specified settings. 23847 * 23848 * @constructor 23849 * @param {Object} settings Name/value object with settings. 23850 * @setting {Boolean} autohide Automatically hide the panel. 23851 */ 23852 init: function(settings) { 23853 var self = this; 23854 23855 function reorder() { 23856 var i, zIndex = FloatPanel.zIndex || 0xFFFF, topModal; 23857 23858 if (zOrder.length) { 23859 for (i = 0; i < zOrder.length; i++) { 23860 if (zOrder[i].modal) { 23861 zIndex++; 23862 topModal = zOrder[i]; 23863 } 23864 23865 zOrder[i].getEl().style.zIndex = zIndex; 23866 zOrder[i].zIndex = zIndex; 23867 zIndex++; 23868 } 23869 } 23870 23871 var modalBlockEl = document.getElementById(self.classPrefix + 'modal-block'); 23872 23873 if (topModal) { 23874 DomUtils.css(modalBlockEl, 'z-index', topModal.zIndex - 1); 23875 } else if (modalBlockEl) { 23876 modalBlockEl.parentNode.removeChild(modalBlockEl); 23877 hasModal = false; 23878 } 23879 23880 FloatPanel.currentZIndex = zIndex; 23881 } 23882 23883 function isChildOf(ctrl, parent) { 23884 while (ctrl) { 23885 if (ctrl == parent) { 23886 return true; 23887 } 23888 23889 ctrl = ctrl.parent(); 23890 } 23891 } 23892 23893 /** 23894 * Repositions the panel to the top of page if the panel is outside of the visual viewport. It will 23895 * also reposition all child panels of the current panel. 23896 */ 23897 function repositionPanel(panel) { 23898 var scrollY = DomUtils.getViewPort().y; 23899 23900 function toggleFixedChildPanels(fixed, deltaY) { 23901 var parent; 23902 23903 for (var i = 0; i < visiblePanels.length; i++) { 23904 if (visiblePanels[i] != panel) { 23905 parent = visiblePanels[i].parent(); 23906 23907 while (parent && (parent = parent.parent())) { 23908 if (parent == panel) { 23909 visiblePanels[i].fixed(fixed).moveBy(0, deltaY).repaint(); 23910 } 23911 } 23912 } 23913 } 23914 } 23915 23916 if (panel.settings.autofix) { 23917 if (!panel._fixed) { 23918 panel._autoFixY = panel.layoutRect().y; 23919 23920 if (panel._autoFixY < scrollY) { 23921 panel.fixed(true).layoutRect({y: 0}).repaint(); 23922 toggleFixedChildPanels(true, scrollY - panel._autoFixY); 23923 } 23924 } else { 23925 if (panel._autoFixY > scrollY) { 23926 panel.fixed(false).layoutRect({y: panel._autoFixY}).repaint(); 23927 toggleFixedChildPanels(false, panel._autoFixY - scrollY); 23928 } 23929 } 23930 } 23931 } 23932 23933 self._super(settings); 23934 self._eventsRoot = self; 23935 23936 self.addClass('floatpanel'); 23937 23938 // Hide floatpanes on click out side the root button 23939 if (settings.autohide) { 23940 if (!documentClickHandler) { 23941 documentClickHandler = function(e) { 23942 // Hide any float panel when a click is out side that float panel and the 23943 // float panels direct parent for example a click on a menu button 23944 var i = visiblePanels.length; 23945 while (i--) { 23946 var panel = visiblePanels[i], clickCtrl = panel.getParentCtrl(e.target); 23947 23948 if (panel.settings.autohide) { 23949 if (clickCtrl) { 23950 if (isChildOf(clickCtrl, panel) || panel.parent() === clickCtrl) { 23951 continue; 23952 } 23953 } 23954 23955 e = panel.fire('autohide', {target: e.target}); 23956 if (!e.isDefaultPrevented()) { 23957 panel.hide(); 23958 } 23959 } 23960 } 23961 }; 23962 23963 DomUtils.on(document, 'click', documentClickHandler); 23964 } 23965 23966 visiblePanels.push(self); 23967 } 23968 23969 if (settings.autofix) { 23970 if (!documentScrollHandler) { 23971 documentScrollHandler = function() { 23972 var i; 23973 23974 i = visiblePanels.length; 23975 while (i--) { 23976 repositionPanel(visiblePanels[i]); 23977 } 23978 }; 23979 23980 DomUtils.on(window, 'scroll', documentScrollHandler); 23981 } 23982 23983 self.on('move', function() { 23984 repositionPanel(this); 23985 }); 23986 } 23987 23988 self.on('postrender show', function(e) { 23989 if (e.control == self) { 23990 var modalBlockEl, prefix = self.classPrefix; 23991 23992 if (self.modal && !hasModal) { 23993 modalBlockEl = DomUtils.createFragment('<div id="' + prefix + 'modal-block" class="' + 23994 prefix + 'reset ' + prefix + 'fade"></div>'); 23995 modalBlockEl = modalBlockEl.firstChild; 23996 23997 self.getContainerElm().appendChild(modalBlockEl); 23998 23999 setTimeout(function() { 24000 DomUtils.addClass(modalBlockEl, prefix + 'in'); 24001 DomUtils.addClass(self.getEl(), prefix + 'in'); 24002 }, 0); 24003 24004 hasModal = true; 24005 } 24006 24007 zOrder.push(self); 24008 reorder(); 24009 } 24010 }); 24011 24012 self.on('close hide', function(e) { 24013 if (e.control == self) { 24014 var i = zOrder.length; 24015 24016 while (i--) { 24017 if (zOrder[i] === self) { 24018 zOrder.splice(i, 1); 24019 } 24020 } 24021 24022 reorder(); 24023 } 24024 }); 24025 24026 self.on('show', function() { 24027 self.parents().each(function(ctrl) { 24028 if (ctrl._fixed) { 24029 self.fixed(true); 24030 return false; 24031 } 24032 }); 24033 }); 24034 24035 if (settings.popover) { 24036 self._preBodyHtml = '<div class="' + self.classPrefix + 'arrow"></div>'; 24037 self.addClass('popover').addClass('bottom').addClass(self.isRtl() ? 'end' : 'start'); 24038 } 24039 }, 24040 24041 fixed: function(state) { 24042 var self = this; 24043 24044 if (self._fixed != state) { 24045 if (self._rendered) { 24046 var viewport = DomUtils.getViewPort(); 24047 24048 if (state) { 24049 self.layoutRect().y -= viewport.y; 24050 } else { 24051 self.layoutRect().y += viewport.y; 24052 } 24053 } 24054 24055 self.toggleClass('fixed', state); 24056 self._fixed = state; 24057 } 24058 24059 return self; 24060 }, 24061 24062 /** 24063 * Shows the current float panel. 24064 * 24065 * @method show 24066 * @return {tinymce.ui.FloatPanel} Current floatpanel instance. 24067 */ 24068 show: function() { 24069 var self = this, i, state = self._super(); 24070 24071 i = visiblePanels.length; 24072 while (i--) { 24073 if (visiblePanels[i] === self) { 24074 break; 24075 } 24076 } 24077 24078 if (i === -1) { 24079 visiblePanels.push(self); 24080 } 24081 24082 return state; 24083 }, 24084 24085 /** 24086 * Hides the current float panel. 24087 * 24088 * @method hide 24089 * @return {tinymce.ui.FloatPanel} Current floatpanel instance. 24090 */ 24091 hide: function() { 24092 removeVisiblePanel(this); 24093 return this._super(); 24094 }, 24095 24096 /** 24097 * Hides all visible the float panels. 24098 * 24099 * @method hideAll 24100 */ 24101 hideAll: function() { 24102 FloatPanel.hideAll(); 24103 }, 24104 24105 /** 24106 * Closes the float panel. This will remove the float panel from page and fire the close event. 24107 * 24108 * @method close 24109 */ 24110 close: function() { 24111 var self = this; 24112 24113 self.fire('close'); 24114 24115 return self.remove(); 24116 }, 24117 24118 /** 24119 * Removes the float panel from page. 24120 * 24121 * @method remove 24122 */ 24123 remove: function() { 24124 removeVisiblePanel(this); 24125 this._super(); 24126 }, 24127 24128 postRender: function() { 24129 var self = this; 24130 24131 if (self.settings.bodyRole) { 24132 this.getEl('body').setAttribute('role', self.settings.bodyRole); 24133 } 24134 24135 return self._super(); 24136 } 24137 }); 24138 24139 /** 24140 * Hides all visible the float panels. 24141 * 24142 * @static 24143 * @method hideAll 24144 */ 24145 FloatPanel.hideAll = function() { 24146 var i = visiblePanels.length; 24147 24148 while (i--) { 24149 var panel = visiblePanels[i]; 24150 24151 if (panel && panel.settings.autohide) { 24152 panel.hide(); 24153 visiblePanels.splice(i, 1); 24154 } 24155 } 24156 }; 24157 24158 function removeVisiblePanel(panel) { 24159 var i; 24160 24161 i = visiblePanels.length; 24162 while (i--) { 24163 if (visiblePanels[i] === panel) { 24164 visiblePanels.splice(i, 1); 24165 } 24166 } 24167 24168 i = zOrder.length; 24169 while (i--) { 24170 if (zOrder[i] === panel) { 24171 zOrder.splice(i, 1); 24172 } 24173 } 24174 } 24175 24176 return FloatPanel; 24177 }); 24178 24179 // Included from: js/tinymce/classes/ui/Window.js 24180 24181 /** 24182 * Window.js 24183 * 24184 * Copyright, Moxiecode Systems AB 24185 * Released under LGPL License. 24186 * 24187 * License: http://www.tinymce.com/license 24188 * Contributing: http://www.tinymce.com/contributing 24189 */ 24190 24191 /** 24192 * Creates a new window. 24193 * 24194 * @-x-less Window.less 24195 * @class tinymce.ui.Window 24196 * @extends tinymce.ui.FloatPanel 24197 */ 24198 define("tinymce/ui/Window", [ 24199 "tinymce/ui/FloatPanel", 24200 "tinymce/ui/Panel", 24201 "tinymce/ui/DomUtils", 24202 "tinymce/ui/DragHelper" 24203 ], function(FloatPanel, Panel, DomUtils, DragHelper) { 24204 "use strict"; 24205 24206 var Window = FloatPanel.extend({ 24207 modal: true, 24208 24209 Defaults: { 24210 border: 1, 24211 layout: 'flex', 24212 containerCls: 'panel', 24213 role: 'dialog', 24214 callbacks: { 24215 submit: function() { 24216 this.fire('submit', {data: this.toJSON()}); 24217 }, 24218 24219 close: function() { 24220 this.close(); 24221 } 24222 } 24223 }, 24224 24225 /** 24226 * Constructs a instance with the specified settings. 24227 * 24228 * @constructor 24229 * @param {Object} settings Name/value object with settings. 24230 */ 24231 init: function(settings) { 24232 var self = this; 24233 24234 self._super(settings); 24235 24236 if (self.isRtl()) { 24237 self.addClass('rtl'); 24238 } 24239 24240 self.addClass('window'); 24241 self._fixed = true; 24242 24243 // Create statusbar 24244 if (settings.buttons) { 24245 self.statusbar = new Panel({ 24246 layout: 'flex', 24247 border: '1 0 0 0', 24248 spacing: 3, 24249 padding: 10, 24250 align: 'center', 24251 pack: self.isRtl() ? 'start' : 'end', 24252 defaults: { 24253 type: 'button' 24254 }, 24255 items: settings.buttons 24256 }); 24257 24258 self.statusbar.addClass('foot'); 24259 self.statusbar.parent(self); 24260 } 24261 24262 self.on('click', function(e) { 24263 if (e.target.className.indexOf(self.classPrefix + 'close') != -1) { 24264 self.close(); 24265 } 24266 }); 24267 24268 self.on('cancel', function() { 24269 self.close(); 24270 }); 24271 24272 self.aria('describedby', self.describedBy || self._id + '-none'); 24273 self.aria('label', settings.title); 24274 self._fullscreen = false; 24275 }, 24276 24277 /** 24278 * Recalculates the positions of the controls in the current container. 24279 * This is invoked by the reflow method and shouldn't be called directly. 24280 * 24281 * @method recalc 24282 */ 24283 recalc: function() { 24284 var self = this, statusbar = self.statusbar, layoutRect, width, x, needsRecalc; 24285 24286 if (self._fullscreen) { 24287 self.layoutRect(DomUtils.getWindowSize()); 24288 self.layoutRect().contentH = self.layoutRect().innerH; 24289 } 24290 24291 self._super(); 24292 24293 layoutRect = self.layoutRect(); 24294 24295 // Resize window based on title width 24296 if (self.settings.title && !self._fullscreen) { 24297 width = layoutRect.headerW; 24298 if (width > layoutRect.w) { 24299 x = layoutRect.x - Math.max(0, width / 2); 24300 self.layoutRect({w: width, x: x}); 24301 needsRecalc = true; 24302 } 24303 } 24304 24305 // Resize window based on statusbar width 24306 if (statusbar) { 24307 statusbar.layoutRect({w: self.layoutRect().innerW}).recalc(); 24308 24309 width = statusbar.layoutRect().minW + layoutRect.deltaW; 24310 if (width > layoutRect.w) { 24311 x = layoutRect.x - Math.max(0, width - layoutRect.w); 24312 self.layoutRect({w: width, x: x}); 24313 needsRecalc = true; 24314 } 24315 } 24316 24317 // Recalc body and disable auto resize 24318 if (needsRecalc) { 24319 self.recalc(); 24320 } 24321 }, 24322 24323 /** 24324 * Initializes the current controls layout rect. 24325 * This will be executed by the layout managers to determine the 24326 * default minWidth/minHeight etc. 24327 * 24328 * @method initLayoutRect 24329 * @return {Object} Layout rect instance. 24330 */ 24331 initLayoutRect: function() { 24332 var self = this, layoutRect = self._super(), deltaH = 0, headEl; 24333 24334 // Reserve vertical space for title 24335 if (self.settings.title && !self._fullscreen) { 24336 headEl = self.getEl('head'); 24337 24338 var size = DomUtils.getSize(headEl); 24339 24340 layoutRect.headerW = size.width; 24341 layoutRect.headerH = size.height; 24342 24343 deltaH += layoutRect.headerH; 24344 } 24345 24346 // Reserve vertical space for statusbar 24347 if (self.statusbar) { 24348 deltaH += self.statusbar.layoutRect().h; 24349 } 24350 24351 layoutRect.deltaH += deltaH; 24352 layoutRect.minH += deltaH; 24353 //layoutRect.innerH -= deltaH; 24354 layoutRect.h += deltaH; 24355 24356 var rect = DomUtils.getWindowSize(); 24357 24358 layoutRect.x = Math.max(0, rect.w / 2 - layoutRect.w / 2); 24359 layoutRect.y = Math.max(0, rect.h / 2 - layoutRect.h / 2); 24360 24361 return layoutRect; 24362 }, 24363 24364 /** 24365 * Renders the control as a HTML string. 24366 * 24367 * @method renderHtml 24368 * @return {String} HTML representing the control. 24369 */ 24370 renderHtml: function() { 24371 var self = this, layout = self._layout, id = self._id, prefix = self.classPrefix; 24372 var settings = self.settings, headerHtml = '', footerHtml = '', html = settings.html; 24373 24374 self.preRender(); 24375 layout.preRender(self); 24376 24377 if (settings.title) { 24378 headerHtml = ( 24379 '<div id="' + id + '-head" class="' + prefix + 'window-head">' + 24380 '<div id="' + id + '-title" class="' + prefix + 'title">' + self.encode(settings.title) + '</div>' + 24381 '<button type="button" class="' + prefix + 'close" aria-hidden="true">\u00d7</button>' + 24382 '<div id="' + id + '-dragh" class="' + prefix + 'dragh"></div>' + 24383 '</div>' 24384 ); 24385 } 24386 24387 if (settings.url) { 24388 html = '<iframe src="' + settings.url + '" tabindex="-1"></iframe>'; 24389 } 24390 24391 if (typeof(html) == "undefined") { 24392 html = layout.renderHtml(self); 24393 } 24394 24395 if (self.statusbar) { 24396 footerHtml = self.statusbar.renderHtml(); 24397 } 24398 24399 return ( 24400 '<div id="' + id + '" class="' + self.classes() + '" hidefocus="1">' + 24401 '<div class="' + self.classPrefix + 'reset" role="application">' + 24402 headerHtml + 24403 '<div id="' + id + '-body" class="' + self.classes('body') + '">' + 24404 html + 24405 '</div>' + 24406 footerHtml + 24407 '</div>' + 24408 '</div>' 24409 ); 24410 }, 24411 24412 /** 24413 * Switches the window fullscreen mode. 24414 * 24415 * @method fullscreen 24416 * @param {Boolean} state True/false state. 24417 * @return {tinymce.ui.Window} Current window instance. 24418 */ 24419 fullscreen: function(state) { 24420 var self = this, documentElement = document.documentElement, slowRendering, prefix = self.classPrefix, layoutRect; 24421 24422 if (state != self._fullscreen) { 24423 DomUtils.on(window, 'resize', function() { 24424 var time; 24425 24426 if (self._fullscreen) { 24427 // Time the layout time if it's to slow use a timeout to not hog the CPU 24428 if (!slowRendering) { 24429 time = new Date().getTime(); 24430 24431 var rect = DomUtils.getWindowSize(); 24432 self.moveTo(0, 0).resizeTo(rect.w, rect.h); 24433 24434 if ((new Date().getTime()) - time > 50) { 24435 slowRendering = true; 24436 } 24437 } else { 24438 if (!self._timer) { 24439 self._timer = setTimeout(function() { 24440 var rect = DomUtils.getWindowSize(); 24441 self.moveTo(0, 0).resizeTo(rect.w, rect.h); 24442 24443 self._timer = 0; 24444 }, 50); 24445 } 24446 } 24447 } 24448 }); 24449 24450 layoutRect = self.layoutRect(); 24451 self._fullscreen = state; 24452 24453 if (!state) { 24454 self._borderBox = self.parseBox(self.settings.border); 24455 self.getEl('head').style.display = ''; 24456 layoutRect.deltaH += layoutRect.headerH; 24457 DomUtils.removeClass(documentElement, prefix + 'fullscreen'); 24458 DomUtils.removeClass(document.body, prefix + 'fullscreen'); 24459 self.removeClass('fullscreen'); 24460 self.moveTo(self._initial.x, self._initial.y).resizeTo(self._initial.w, self._initial.h); 24461 } else { 24462 self._initial = {x: layoutRect.x, y: layoutRect.y, w: layoutRect.w, h: layoutRect.h}; 24463 24464 self._borderBox = self.parseBox('0'); 24465 self.getEl('head').style.display = 'none'; 24466 layoutRect.deltaH -= layoutRect.headerH + 2; 24467 DomUtils.addClass(documentElement, prefix + 'fullscreen'); 24468 DomUtils.addClass(document.body, prefix + 'fullscreen'); 24469 self.addClass('fullscreen'); 24470 24471 var rect = DomUtils.getWindowSize(); 24472 self.moveTo(0, 0).resizeTo(rect.w, rect.h); 24473 } 24474 } 24475 24476 return self.reflow(); 24477 }, 24478 24479 /** 24480 * Called after the control has been rendered. 24481 * 24482 * @method postRender 24483 */ 24484 postRender: function() { 24485 var self = this, startPos; 24486 24487 setTimeout(function() { 24488 self.addClass('in'); 24489 }, 0); 24490 24491 self._super(); 24492 24493 if (self.statusbar) { 24494 self.statusbar.postRender(); 24495 } 24496 24497 self.focus(); 24498 24499 this.dragHelper = new DragHelper(self._id + '-dragh', { 24500 start: function() { 24501 startPos = { 24502 x: self.layoutRect().x, 24503 y: self.layoutRect().y 24504 }; 24505 }, 24506 24507 drag: function(e) { 24508 self.moveTo(startPos.x + e.deltaX, startPos.y + e.deltaY); 24509 } 24510 }); 24511 24512 self.on('submit', function(e) { 24513 if (!e.isDefaultPrevented()) { 24514 self.close(); 24515 } 24516 }); 24517 }, 24518 24519 /** 24520 * Fires a submit event with the serialized form. 24521 * 24522 * @method submit 24523 * @return {Object} Event arguments object. 24524 */ 24525 submit: function() { 24526 return this.fire('submit', {data: this.toJSON()}); 24527 }, 24528 24529 /** 24530 * Removes the current control from DOM and from UI collections. 24531 * 24532 * @method remove 24533 * @return {tinymce.ui.Control} Current control instance. 24534 */ 24535 remove: function() { 24536 var self = this, prefix = self.classPrefix; 24537 24538 self.dragHelper.destroy(); 24539 self._super(); 24540 24541 if (self.statusbar) { 24542 this.statusbar.remove(); 24543 } 24544 24545 if (self._fullscreen) { 24546 DomUtils.removeClass(document.documentElement, prefix + 'fullscreen'); 24547 DomUtils.removeClass(document.body, prefix + 'fullscreen'); 24548 } 24549 }, 24550 24551 /** 24552 * Returns the contentWindow object of the iframe if it exists. 24553 * 24554 * @method getContentWindow 24555 * @return {Window} window object or null. 24556 */ 24557 getContentWindow: function() { 24558 var ifr = this.getEl().getElementsByTagName('iframe')[0]; 24559 return ifr ? ifr.contentWindow : null; 24560 } 24561 }); 24562 24563 return Window; 24564 }); 24565 24566 // Included from: js/tinymce/classes/ui/MessageBox.js 24567 24568 /** 24569 * MessageBox.js 24570 * 24571 * Copyright, Moxiecode Systems AB 24572 * Released under LGPL License. 24573 * 24574 * License: http://www.tinymce.com/license 24575 * Contributing: http://www.tinymce.com/contributing 24576 */ 24577 24578 /** 24579 * This class is used to create MessageBoxes like alerts/confirms etc. 24580 * 24581 * @class tinymce.ui.Window 24582 * @extends tinymce.ui.FloatPanel 24583 */ 24584 define("tinymce/ui/MessageBox", [ 24585 "tinymce/ui/Window" 24586 ], function(Window) { 24587 "use strict"; 24588 24589 var MessageBox = Window.extend({ 24590 /** 24591 * Constructs a instance with the specified settings. 24592 * 24593 * @constructor 24594 * @param {Object} settings Name/value object with settings. 24595 */ 24596 init: function(settings) { 24597 settings = { 24598 border: 1, 24599 padding: 20, 24600 layout: 'flex', 24601 pack: "center", 24602 align: "center", 24603 containerCls: 'panel', 24604 autoScroll: true, 24605 buttons: {type: "button", text: "Ok", action: "ok"}, 24606 items: { 24607 type: "label", 24608 multiline: true, 24609 maxWidth: 500, 24610 maxHeight: 200 24611 } 24612 }; 24613 24614 this._super(settings); 24615 }, 24616 24617 Statics: { 24618 /** 24619 * Ok buttons constant. 24620 * 24621 * @static 24622 * @final 24623 * @field {Number} OK 24624 */ 24625 OK: 1, 24626 24627 /** 24628 * Ok/cancel buttons constant. 24629 * 24630 * @static 24631 * @final 24632 * @field {Number} OK_CANCEL 24633 */ 24634 OK_CANCEL: 2, 24635 24636 /** 24637 * yes/no buttons constant. 24638 * 24639 * @static 24640 * @final 24641 * @field {Number} YES_NO 24642 */ 24643 YES_NO: 3, 24644 24645 /** 24646 * yes/no/cancel buttons constant. 24647 * 24648 * @static 24649 * @final 24650 * @field {Number} YES_NO_CANCEL 24651 */ 24652 YES_NO_CANCEL: 4, 24653 24654 /** 24655 * Constructs a new message box and renders it to the body element. 24656 * 24657 * @static 24658 * @method msgBox 24659 * @param {Object} settings Name/value object with settings. 24660 */ 24661 msgBox: function(settings) { 24662 var buttons, callback = settings.callback || function() {}; 24663 24664 switch (settings.buttons) { 24665 case MessageBox.OK_CANCEL: 24666 buttons = [ 24667 {type: "button", text: "Ok", subtype: "primary", onClick: function(e) { 24668 e.control.parents()[1].close(); 24669 callback(true); 24670 }}, 24671 24672 {type: "button", text: "Cancel", onClick: function(e) { 24673 e.control.parents()[1].close(); 24674 callback(false); 24675 }} 24676 ]; 24677 break; 24678 24679 case MessageBox.YES_NO: 24680 buttons = [ 24681 {type: "button", text: "Ok", subtype: "primary", onClick: function(e) { 24682 e.control.parents()[1].close(); 24683 callback(true); 24684 }} 24685 ]; 24686 break; 24687 24688 case MessageBox.YES_NO_CANCEL: 24689 buttons = [ 24690 {type: "button", text: "Ok", subtype: "primary", onClick: function(e) { 24691 e.control.parents()[1].close(); 24692 }} 24693 ]; 24694 break; 24695 24696 default: 24697 buttons = [ 24698 {type: "button", text: "Ok", subtype: "primary", onClick: function(e) { 24699 e.control.parents()[1].close(); 24700 callback(true); 24701 }} 24702 ]; 24703 break; 24704 } 24705 24706 return new Window({ 24707 padding: 20, 24708 x: settings.x, 24709 y: settings.y, 24710 minWidth: 300, 24711 minHeight: 100, 24712 layout: "flex", 24713 pack: "center", 24714 align: "center", 24715 buttons: buttons, 24716 title: settings.title, 24717 role: 'alertdialog', 24718 items: { 24719 type: "label", 24720 multiline: true, 24721 maxWidth: 500, 24722 maxHeight: 200, 24723 text: settings.text 24724 }, 24725 onPostRender: function() { 24726 this.aria('describedby', this.items()[0]._id); 24727 }, 24728 onClose: settings.onClose, 24729 onCancel: function() { 24730 callback(false); 24731 } 24732 }).renderTo(document.body).reflow(); 24733 }, 24734 24735 /** 24736 * Creates a new alert dialog. 24737 * 24738 * @method alert 24739 * @param {Object} settings Settings for the alert dialog. 24740 * @param {function} [callback] Callback to execute when the user makes a choice. 24741 */ 24742 alert: function(settings, callback) { 24743 if (typeof(settings) == "string") { 24744 settings = {text: settings}; 24745 } 24746 24747 settings.callback = callback; 24748 return MessageBox.msgBox(settings); 24749 }, 24750 24751 /** 24752 * Creates a new confirm dialog. 24753 * 24754 * @method confirm 24755 * @param {Object} settings Settings for the confirm dialog. 24756 * @param {function} [callback] Callback to execute when the user makes a choice. 24757 */ 24758 confirm: function(settings, callback) { 24759 if (typeof(settings) == "string") { 24760 settings = {text: settings}; 24761 } 24762 24763 settings.callback = callback; 24764 settings.buttons = MessageBox.OK_CANCEL; 24765 24766 return MessageBox.msgBox(settings); 24767 } 24768 } 24769 }); 24770 24771 return MessageBox; 24772 }); 24773 24774 // Included from: js/tinymce/classes/WindowManager.js 24775 24776 /** 24777 * WindowManager.js 24778 * 24779 * Copyright, Moxiecode Systems AB 24780 * Released under LGPL License. 24781 * 24782 * License: http://www.tinymce.com/license 24783 * Contributing: http://www.tinymce.com/contributing 24784 */ 24785 24786 /** 24787 * This class handles the creation of native windows and dialogs. This class can be extended to provide for example inline dialogs. 24788 * 24789 * @class tinymce.WindowManager 24790 * @example 24791 * // Opens a new dialog with the file.htm file and the size 320x240 24792 * // It also adds a custom parameter this can be retrieved by using tinyMCEPopup.getWindowArg inside the dialog. 24793 * tinymce.activeEditor.windowManager.open({ 24794 * url: 'file.htm', 24795 * width: 320, 24796 * height: 240 24797 * }, { 24798 * custom_param: 1 24799 * }); 24800 * 24801 * // Displays an alert box using the active editors window manager instance 24802 * tinymce.activeEditor.windowManager.alert('Hello world!'); 24803 * 24804 * // Displays an confirm box and an alert message will be displayed depending on what you choose in the confirm 24805 * tinymce.activeEditor.windowManager.confirm("Do you want to do something", function(s) { 24806 * if (s) 24807 * tinymce.activeEditor.windowManager.alert("Ok"); 24808 * else 24809 * tinymce.activeEditor.windowManager.alert("Cancel"); 24810 * }); 24811 */ 24812 define("tinymce/WindowManager", [ 24813 "tinymce/ui/Window", 24814 "tinymce/ui/MessageBox" 24815 ], function(Window, MessageBox) { 24816 return function(editor) { 24817 var self = this, windows = []; 24818 24819 function getTopMostWindow() { 24820 if (windows.length) { 24821 return windows[windows.length - 1]; 24822 } 24823 } 24824 24825 self.windows = windows; 24826 24827 /** 24828 * Opens a new window. 24829 * 24830 * @method open 24831 * @param {Object} args Optional name/value settings collection contains things like width/height/url etc. 24832 * @option {String} title Window title. 24833 * @option {String} file URL of the file to open in the window. 24834 * @option {Number} width Width in pixels. 24835 * @option {Number} height Height in pixels. 24836 * @option {Boolean} resizable Specifies whether the popup window is resizable or not. 24837 * @option {Boolean} maximizable Specifies whether the popup window has a "maximize" button and can get maximized or not. 24838 * @option {String/Boolean} scrollbars Specifies whether the popup window can have scrollbars if required (i.e. content 24839 * larger than the popup size specified). 24840 */ 24841 self.open = function(args, params) { 24842 var win; 24843 24844 editor.editorManager.activeEditor = editor; 24845 24846 args.title = args.title || ' '; 24847 24848 // Handle URL 24849 args.url = args.url || args.file; // Legacy 24850 if (args.url) { 24851 args.width = parseInt(args.width || 320, 10); 24852 args.height = parseInt(args.height || 240, 10); 24853 } 24854 24855 // Handle body 24856 if (args.body) { 24857 args.items = { 24858 defaults: args.defaults, 24859 type: args.bodyType || 'form', 24860 items: args.body 24861 }; 24862 } 24863 24864 if (!args.url && !args.buttons) { 24865 args.buttons = [ 24866 {text: 'Ok', subtype: 'primary', onclick: function() { 24867 win.find('form')[0].submit(); 24868 }}, 24869 24870 {text: 'Cancel', onclick: function() { 24871 win.close(); 24872 }} 24873 ]; 24874 } 24875 24876 win = new Window(args); 24877 windows.push(win); 24878 24879 win.on('close', function() { 24880 var i = windows.length; 24881 24882 while (i--) { 24883 if (windows[i] === win) { 24884 windows.splice(i, 1); 24885 } 24886 } 24887 24888 editor.focus(); 24889 }); 24890 24891 // Handle data 24892 if (args.data) { 24893 win.on('postRender', function() { 24894 this.find('*').each(function(ctrl) { 24895 var name = ctrl.name(); 24896 24897 if (name in args.data) { 24898 ctrl.value(args.data[name]); 24899 } 24900 }); 24901 }); 24902 } 24903 24904 // store args and parameters 24905 win.features = args || {}; 24906 win.params = params || {}; 24907 24908 // Takes a snapshot in the FocusManager of the selection before focus is lost to dialog 24909 editor.nodeChanged(); 24910 24911 return win.renderTo().reflow(); 24912 }; 24913 24914 /** 24915 * Creates a alert dialog. Please don't use the blocking behavior of this 24916 * native version use the callback method instead then it can be extended. 24917 * 24918 * @method alert 24919 * @param {String} message Text to display in the new alert dialog. 24920 * @param {function} callback Callback function to be executed after the user has selected ok. 24921 * @param {Object} scope Optional scope to execute the callback in. 24922 * @example 24923 * // Displays an alert box using the active editors window manager instance 24924 * tinymce.activeEditor.windowManager.alert('Hello world!'); 24925 */ 24926 self.alert = function(message, callback, scope) { 24927 MessageBox.alert(message, function() { 24928 if (callback) { 24929 callback.call(scope || this); 24930 } else { 24931 editor.focus(); 24932 } 24933 }); 24934 }; 24935 24936 /** 24937 * Creates a confirm dialog. Please don't use the blocking behavior of this 24938 * native version use the callback method instead then it can be extended. 24939 * 24940 * @method confirm 24941 * @param {String} messageText to display in the new confirm dialog. 24942 * @param {function} callback Callback function to be executed after the user has selected ok or cancel. 24943 * @param {Object} scope Optional scope to execute the callback in. 24944 * @example 24945 * // Displays an confirm box and an alert message will be displayed depending on what you choose in the confirm 24946 * tinymce.activeEditor.windowManager.confirm("Do you want to do something", function(s) { 24947 * if (s) 24948 * tinymce.activeEditor.windowManager.alert("Ok"); 24949 * else 24950 * tinymce.activeEditor.windowManager.alert("Cancel"); 24951 * }); 24952 */ 24953 self.confirm = function(message, callback, scope) { 24954 MessageBox.confirm(message, function(state) { 24955 callback.call(scope || this, state); 24956 }); 24957 }; 24958 24959 /** 24960 * Closes the top most window. 24961 * 24962 * @method close 24963 */ 24964 self.close = function() { 24965 if (getTopMostWindow()) { 24966 getTopMostWindow().close(); 24967 } 24968 }; 24969 24970 /** 24971 * Returns the params of the last window open call. This can be used in iframe based 24972 * dialog to get params passed from the tinymce plugin. 24973 * 24974 * @example 24975 * var dialogArguments = top.tinymce.activeEditor.windowManager.getParams(); 24976 * 24977 * @method getParams 24978 * @return {Object} Name/value object with parameters passed from windowManager.open call. 24979 */ 24980 self.getParams = function() { 24981 return getTopMostWindow() ? getTopMostWindow().params : null; 24982 }; 24983 24984 /** 24985 * Sets the params of the last opened window. 24986 * 24987 * @method setParams 24988 * @param {Object} params Params object to set for the last opened window. 24989 */ 24990 self.setParams = function(params) { 24991 if (getTopMostWindow()) { 24992 getTopMostWindow().params = params; 24993 } 24994 }; 24995 24996 /** 24997 * Returns the currently opened window objects. 24998 * 24999 * @method getWindows 25000 * @return {Array} Array of the currently opened windows. 25001 */ 25002 self.getWindows = function() { 25003 return windows; 25004 }; 25005 }; 25006 }); 25007 25008 // Included from: js/tinymce/classes/util/Quirks.js 25009 25010 /** 25011 * Quirks.js 25012 * 25013 * Copyright, Moxiecode Systems AB 25014 * Released under LGPL License. 25015 * 25016 * License: http://www.tinymce.com/license 25017 * Contributing: http://www.tinymce.com/contributing 25018 * 25019 * @ignore-file 25020 */ 25021 25022 /** 25023 * This file includes fixes for various browser quirks it's made to make it easy to add/remove browser specific fixes. 25024 * 25025 * @class tinymce.util.Quirks 25026 */ 25027 define("tinymce/util/Quirks", [ 25028 "tinymce/util/VK", 25029 "tinymce/dom/RangeUtils", 25030 "tinymce/html/Node", 25031 "tinymce/html/Entities", 25032 "tinymce/Env", 25033 "tinymce/util/Tools" 25034 ], function(VK, RangeUtils, Node, Entities, Env, Tools) { 25035 return function(editor) { 25036 var each = Tools.each; 25037 var BACKSPACE = VK.BACKSPACE, DELETE = VK.DELETE, dom = editor.dom, selection = editor.selection, 25038 settings = editor.settings, parser = editor.parser, serializer = editor.serializer; 25039 var isGecko = Env.gecko, isIE = Env.ie, isWebKit = Env.webkit; 25040 25041 /** 25042 * Executes a command with a specific state this can be to enable/disable browser editing features. 25043 */ 25044 function setEditorCommandState(cmd, state) { 25045 try { 25046 editor.getDoc().execCommand(cmd, false, state); 25047 } catch (ex) { 25048 // Ignore 25049 } 25050 } 25051 25052 /** 25053 * Returns current IE document mode. 25054 */ 25055 function getDocumentMode() { 25056 var documentMode = editor.getDoc().documentMode; 25057 25058 return documentMode ? documentMode : 6; 25059 } 25060 25061 /** 25062 * Returns true/false if the event is prevented or not. 25063 * 25064 * @private 25065 * @param {Event} e Event object. 25066 * @return {Boolean} true/false if the event is prevented or not. 25067 */ 25068 function isDefaultPrevented(e) { 25069 return e.isDefaultPrevented(); 25070 } 25071 25072 /** 25073 * Fixes a WebKit bug when deleting contents using backspace or delete key. 25074 * WebKit will produce a span element if you delete across two block elements. 25075 * 25076 * Example: 25077 * <h1>a</h1><p>|b</p> 25078 * 25079 * Will produce this on backspace: 25080 * <h1>a<span style="<all runtime styles>">b</span></p> 25081 * 25082 * This fixes the backspace to produce: 25083 * <h1>a|b</p> 25084 * 25085 * See bug: https://bugs.webkit.org/show_bug.cgi?id=45784 25086 * 25087 * This fixes the following delete scenarios: 25088 * 1. Delete by pressing backspace key. 25089 * 2. Delete by pressing delete key. 25090 * 3. Delete by pressing backspace key with ctrl/cmd (Word delete). 25091 * 4. Delete by pressing delete key with ctrl/cmd (Word delete). 25092 * 5. Delete by drag/dropping contents inside the editor. 25093 * 6. Delete by using Cut Ctrl+X/Cmd+X. 25094 * 7. Delete by selecting contents and writing a character.' 25095 * 25096 * This code is a ugly hack since writing full custom delete logic for just this bug 25097 * fix seemed like a huge task. I hope we can remove this before the year 2030. 25098 */ 25099 function cleanupStylesWhenDeleting() { 25100 var doc = editor.getDoc(), urlPrefix = 'data:text/mce-internal,'; 25101 var MutationObserver = window.MutationObserver, olderWebKit, dragStartRng; 25102 25103 // Add mini polyfill for older WebKits 25104 // TODO: Remove this when old Safari versions gets updated 25105 if (!MutationObserver) { 25106 olderWebKit = true; 25107 25108 MutationObserver = function() { 25109 var records = [], target; 25110 25111 function nodeInsert(e) { 25112 var target = e.relatedNode || e.target; 25113 records.push({target: target, addedNodes: [target]}); 25114 } 25115 25116 function attrModified(e) { 25117 var target = e.relatedNode || e.target; 25118 records.push({target: target, attributeName: e.attrName}); 25119 } 25120 25121 this.observe = function(node) { 25122 target = node; 25123 target.addEventListener('DOMSubtreeModified', nodeInsert, false); 25124 target.addEventListener('DOMNodeInsertedIntoDocument', nodeInsert, false); 25125 target.addEventListener('DOMNodeInserted', nodeInsert, false); 25126 target.addEventListener('DOMAttrModified', attrModified, false); 25127 }; 25128 25129 this.disconnect = function() { 25130 target.removeEventListener('DOMSubtreeModified', nodeInsert, false); 25131 target.removeEventListener('DOMNodeInsertedIntoDocument', nodeInsert, false); 25132 target.removeEventListener('DOMNodeInserted', nodeInsert, false); 25133 target.removeEventListener('DOMAttrModified', attrModified, false); 25134 }; 25135 25136 this.takeRecords = function() { 25137 return records; 25138 }; 25139 }; 25140 } 25141 25142 function customDelete(isForward) { 25143 var mutationObserver = new MutationObserver(function() {}); 25144 25145 Tools.each(editor.getBody().getElementsByTagName('*'), function(elm) { 25146 // Mark existing spans 25147 if (elm.tagName == 'SPAN') { 25148 elm.setAttribute('mce-data-marked', 1); 25149 } 25150 25151 // Make sure all elements has a data-mce-style attribute 25152 if (!elm.hasAttribute('data-mce-style') && elm.hasAttribute('style')) { 25153 editor.dom.setAttrib(elm, 'style', elm.getAttribute('style')); 25154 } 25155 }); 25156 25157 // Observe added nodes and style attribute changes 25158 mutationObserver.observe(editor.getDoc(), { 25159 childList: true, 25160 attributes: true, 25161 subtree: true, 25162 attributeFilter: ['style'] 25163 }); 25164 25165 editor.getDoc().execCommand(isForward ? 'ForwardDelete' : 'Delete', false, null); 25166 25167 var rng = editor.selection.getRng(); 25168 var caretElement = rng.startContainer.parentNode; 25169 25170 Tools.each(mutationObserver.takeRecords(), function(record) { 25171 if (!dom.isChildOf(record.target, editor.getBody())) { 25172 return; 25173 } 25174 25175 // Restore style attribute to previous value 25176 if (record.attributeName == "style") { 25177 var oldValue = record.target.getAttribute('data-mce-style'); 25178 25179 if (oldValue) { 25180 record.target.setAttribute("style", oldValue); 25181 } else { 25182 record.target.removeAttribute("style"); 25183 } 25184 } 25185 25186 // Remove all spans that isn't maked and retain selection 25187 Tools.each(record.addedNodes, function(node) { 25188 if (node.nodeName == "SPAN" && !node.getAttribute('mce-data-marked')) { 25189 var offset, container; 25190 25191 if (node == caretElement) { 25192 offset = rng.startOffset; 25193 container = node.firstChild; 25194 } 25195 25196 dom.remove(node, true); 25197 25198 if (container) { 25199 rng.setStart(container, offset); 25200 rng.setEnd(container, offset); 25201 editor.selection.setRng(rng); 25202 } 25203 } 25204 }); 25205 }); 25206 25207 mutationObserver.disconnect(); 25208 25209 // Remove any left over marks 25210 Tools.each(editor.dom.select('span[mce-data-marked]'), function(span) { 25211 span.removeAttribute('mce-data-marked'); 25212 }); 25213 } 25214 25215 editor.on('keydown', function(e) { 25216 var isForward = e.keyCode == DELETE, isMeta = VK.metaKeyPressed(e); 25217 25218 if (!isDefaultPrevented(e) && (isForward || e.keyCode == BACKSPACE)) { 25219 var rng = editor.selection.getRng(), container = rng.startContainer, offset = rng.startOffset; 25220 25221 // Ignore non meta delete in the where there is text before/after the caret 25222 if (!isMeta && rng.collapsed && container.nodeType == 3) { 25223 if (isForward ? offset < container.data.length : offset > 0) { 25224 return; 25225 } 25226 } 25227 25228 e.preventDefault(); 25229 25230 if (isMeta) { 25231 editor.selection.getSel().modify("extend", isForward ? "forward" : "backward", "word"); 25232 } 25233 25234 customDelete(isForward); 25235 } 25236 }); 25237 25238 editor.on('keypress', function(e) { 25239 if (!isDefaultPrevented(e) && !selection.isCollapsed() && e.charCode && !VK.metaKeyPressed(e)) { 25240 e.preventDefault(); 25241 customDelete(true); 25242 editor.selection.setContent(String.fromCharCode(e.charCode)); 25243 } 25244 }); 25245 25246 editor.addCommand('Delete', function() { 25247 customDelete(); 25248 }); 25249 25250 editor.addCommand('ForwardDelete', function() { 25251 customDelete(true); 25252 }); 25253 25254 // Older WebKits doesn't properly handle the clipboard so we can't add the rest 25255 if (olderWebKit) { 25256 return; 25257 } 25258 25259 editor.on('dragstart', function(e) { 25260 var selectionHtml; 25261 25262 if (editor.selection.isCollapsed() && e.target.tagName == 'IMG') { 25263 selection.select(e.target); 25264 } 25265 25266 dragStartRng = selection.getRng(); 25267 selectionHtml = editor.selection.getContent(); 25268 25269 // Safari doesn't support custom dataTransfer items so we can only use URL and Text 25270 if (selectionHtml.length > 0) { 25271 e.dataTransfer.setData('URL', 'data:text/mce-internal,' + escape(selectionHtml)); 25272 } 25273 }); 25274 25275 editor.on('drop', function(e) { 25276 if (!isDefaultPrevented(e)) { 25277 var internalContent = e.dataTransfer.getData('URL'); 25278 25279 if (!internalContent || internalContent.indexOf(urlPrefix) == -1 || !doc.caretRangeFromPoint) { 25280 return; 25281 } 25282 25283 internalContent = unescape(internalContent.substr(urlPrefix.length)); 25284 if (doc.caretRangeFromPoint) { 25285 e.preventDefault(); 25286 25287 // Safari has a weird issue where drag/dropping images sometimes 25288 // produces a green plus icon. When this happens the caretRangeFromPoint 25289 // will return "null" even though the x, y coordinate is correct. 25290 // But if we detach the insert from the drop event we will get a proper range 25291 window.setTimeout(function() { 25292 var pointRng = doc.caretRangeFromPoint(e.x, e.y); 25293 25294 if (dragStartRng) { 25295 selection.setRng(dragStartRng); 25296 dragStartRng = null; 25297 } 25298 25299 customDelete(); 25300 25301 selection.setRng(pointRng); 25302 editor.insertContent(internalContent); 25303 }, 0); 25304 } 25305 25306 } 25307 }); 25308 25309 editor.on('cut', function(e) { 25310 if (!isDefaultPrevented(e) && e.clipboardData) { 25311 e.preventDefault(); 25312 e.clipboardData.clearData(); 25313 e.clipboardData.setData('text/html', editor.selection.getContent()); 25314 e.clipboardData.setData('text/plain', editor.selection.getContent({format: 'text'})); 25315 customDelete(true); 25316 } 25317 }); 25318 } 25319 25320 /** 25321 * Makes sure that the editor body becomes empty when backspace or delete is pressed in empty editors. 25322 * 25323 * For example: 25324 * <p><b>|</b></p> 25325 * 25326 * Or: 25327 * <h1>|</h1> 25328 * 25329 * Or: 25330 * [<h1></h1>] 25331 */ 25332 function emptyEditorWhenDeleting() { 25333 function serializeRng(rng) { 25334 var body = dom.create("body"); 25335 var contents = rng.cloneContents(); 25336 body.appendChild(contents); 25337 return selection.serializer.serialize(body, {format: 'html'}); 25338 } 25339 25340 function allContentsSelected(rng) { 25341 if (!rng.setStart) { 25342 if (rng.item) { 25343 return false; 25344 } 25345 25346 var bodyRng = rng.duplicate(); 25347 bodyRng.moveToElementText(editor.getBody()); 25348 return RangeUtils.compareRanges(rng, bodyRng); 25349 } 25350 25351 var selection = serializeRng(rng); 25352 25353 var allRng = dom.createRng(); 25354 allRng.selectNode(editor.getBody()); 25355 25356 var allSelection = serializeRng(allRng); 25357 return selection === allSelection; 25358 } 25359 25360 editor.on('keydown', function(e) { 25361 var keyCode = e.keyCode, isCollapsed, body; 25362 25363 // Empty the editor if it's needed for example backspace at <p><b>|</b></p> 25364 if (!isDefaultPrevented(e) && (keyCode == DELETE || keyCode == BACKSPACE)) { 25365 isCollapsed = editor.selection.isCollapsed(); 25366 body = editor.getBody(); 25367 25368 // Selection is collapsed but the editor isn't empty 25369 if (isCollapsed && !dom.isEmpty(body)) { 25370 return; 25371 } 25372 25373 // Selection isn't collapsed but not all the contents is selected 25374 if (!isCollapsed && !allContentsSelected(editor.selection.getRng())) { 25375 return; 25376 } 25377 25378 // Manually empty the editor 25379 e.preventDefault(); 25380 editor.setContent(''); 25381 25382 if (body.firstChild && dom.isBlock(body.firstChild)) { 25383 editor.selection.setCursorLocation(body.firstChild, 0); 25384 } else { 25385 editor.selection.setCursorLocation(body, 0); 25386 } 25387 25388 editor.nodeChanged(); 25389 } 25390 }); 25391 } 25392 25393 /** 25394 * WebKit doesn't select all the nodes in the body when you press Ctrl+A. 25395 * IE selects more than the contents <body>[<p>a</p>]</body> instead of <body><p>[a]</p]</body> see bug #6438 25396 * This selects the whole body so that backspace/delete logic will delete everything 25397 */ 25398 function selectAll() { 25399 editor.on('keydown', function(e) { 25400 if (!isDefaultPrevented(e) && e.keyCode == 65 && VK.metaKeyPressed(e)) { 25401 e.preventDefault(); 25402 editor.execCommand('SelectAll'); 25403 } 25404 }); 25405 } 25406 25407 /** 25408 * WebKit has a weird issue where it some times fails to properly convert keypresses to input method keystrokes. 25409 * The IME on Mac doesn't initialize when it doesn't fire a proper focus event. 25410 * 25411 * This seems to happen when the user manages to click the documentElement element then the window doesn't get proper focus until 25412 * you enter a character into the editor. 25413 * 25414 * It also happens when the first focus in made to the body. 25415 * 25416 * See: https://bugs.webkit.org/show_bug.cgi?id=83566 25417 */ 25418 function inputMethodFocus() { 25419 if (!editor.settings.content_editable) { 25420 // Case 1 IME doesn't initialize if you focus the document 25421 dom.bind(editor.getDoc(), 'focusin', function() { 25422 selection.setRng(selection.getRng()); 25423 }); 25424 25425 // Case 2 IME doesn't initialize if you click the documentElement it also doesn't properly fire the focusin event 25426 dom.bind(editor.getDoc(), 'mousedown', function(e) { 25427 if (e.target == editor.getDoc().documentElement) { 25428 editor.getBody().focus(); 25429 selection.setRng(selection.getRng()); 25430 } 25431 }); 25432 } 25433 } 25434 25435 /** 25436 * Backspacing in FireFox/IE from a paragraph into a horizontal rule results in a floating text node because the 25437 * browser just deletes the paragraph - the browser fails to merge the text node with a horizontal rule so it is 25438 * left there. TinyMCE sees a floating text node and wraps it in a paragraph on the key up event (ForceBlocks.js 25439 * addRootBlocks), meaning the action does nothing. With this code, FireFox/IE matche the behaviour of other 25440 * browsers. 25441 * 25442 * It also fixes a bug on Firefox where it's impossible to delete HR elements. 25443 */ 25444 function removeHrOnBackspace() { 25445 editor.on('keydown', function(e) { 25446 if (!isDefaultPrevented(e) && e.keyCode === BACKSPACE) { 25447 if (selection.isCollapsed() && selection.getRng(true).startOffset === 0) { 25448 var node = selection.getNode(); 25449 var previousSibling = node.previousSibling; 25450 25451 if (node.nodeName == 'HR') { 25452 dom.remove(node); 25453 e.preventDefault(); 25454 return; 25455 } 25456 25457 if (previousSibling && previousSibling.nodeName && previousSibling.nodeName.toLowerCase() === "hr") { 25458 dom.remove(previousSibling); 25459 e.preventDefault(); 25460 } 25461 } 25462 } 25463 }); 25464 } 25465 25466 /** 25467 * Firefox 3.x has an issue where the body element won't get proper focus if you click out 25468 * side it's rectangle. 25469 */ 25470 function focusBody() { 25471 // Fix for a focus bug in FF 3.x where the body element 25472 // wouldn't get proper focus if the user clicked on the HTML element 25473 if (!window.Range.prototype.getClientRects) { // Detect getClientRects got introduced in FF 4 25474 editor.on('mousedown', function(e) { 25475 if (!isDefaultPrevented(e) && e.target.nodeName === "HTML") { 25476 var body = editor.getBody(); 25477 25478 // Blur the body it's focused but not correctly focused 25479 body.blur(); 25480 25481 // Refocus the body after a little while 25482 setTimeout(function() { 25483 body.focus(); 25484 }, 0); 25485 } 25486 }); 25487 } 25488 } 25489 25490 /** 25491 * WebKit has a bug where it isn't possible to select image, hr or anchor elements 25492 * by clicking on them so we need to fake that. 25493 */ 25494 function selectControlElements() { 25495 editor.on('click', function(e) { 25496 e = e.target; 25497 25498 // Workaround for bug, http://bugs.webkit.org/show_bug.cgi?id=12250 25499 // WebKit can't even do simple things like selecting an image 25500 // Needs tobe the setBaseAndExtend or it will fail to select floated images 25501 if (/^(IMG|HR)$/.test(e.nodeName)) { 25502 selection.getSel().setBaseAndExtent(e, 0, e, 1); 25503 } 25504 25505 if (e.nodeName == 'A' && dom.hasClass(e, 'mce-item-anchor')) { 25506 selection.select(e); 25507 } 25508 25509 editor.nodeChanged(); 25510 }); 25511 } 25512 25513 /** 25514 * Fixes a Gecko bug where the style attribute gets added to the wrong element when deleting between two block elements. 25515 * 25516 * Fixes do backspace/delete on this: 25517 * <p>bla[ck</p><p style="color:red">r]ed</p> 25518 * 25519 * Would become: 25520 * <p>bla|ed</p> 25521 * 25522 * Instead of: 25523 * <p style="color:red">bla|ed</p> 25524 */ 25525 function removeStylesWhenDeletingAcrossBlockElements() { 25526 function getAttributeApplyFunction() { 25527 var template = dom.getAttribs(selection.getStart().cloneNode(false)); 25528 25529 return function() { 25530 var target = selection.getStart(); 25531 25532 if (target !== editor.getBody()) { 25533 dom.setAttrib(target, "style", null); 25534 25535 each(template, function(attr) { 25536 target.setAttributeNode(attr.cloneNode(true)); 25537 }); 25538 } 25539 }; 25540 } 25541 25542 function isSelectionAcrossElements() { 25543 return !selection.isCollapsed() && 25544 dom.getParent(selection.getStart(), dom.isBlock) != dom.getParent(selection.getEnd(), dom.isBlock); 25545 } 25546 25547 editor.on('keypress', function(e) { 25548 var applyAttributes; 25549 25550 if (!isDefaultPrevented(e) && (e.keyCode == 8 || e.keyCode == 46) && isSelectionAcrossElements()) { 25551 applyAttributes = getAttributeApplyFunction(); 25552 editor.getDoc().execCommand('delete', false, null); 25553 applyAttributes(); 25554 e.preventDefault(); 25555 return false; 25556 } 25557 }); 25558 25559 dom.bind(editor.getDoc(), 'cut', function(e) { 25560 var applyAttributes; 25561 25562 if (!isDefaultPrevented(e) && isSelectionAcrossElements()) { 25563 applyAttributes = getAttributeApplyFunction(); 25564 25565 setTimeout(function() { 25566 applyAttributes(); 25567 }, 0); 25568 } 25569 }); 25570 } 25571 25572 /** 25573 * Fire a nodeChanged when the selection is changed on WebKit this fixes selection issues on iOS5. It only fires the nodeChange 25574 * event every 50ms since it would other wise update the UI when you type and it hogs the CPU. 25575 */ 25576 function selectionChangeNodeChanged() { 25577 var lastRng, selectionTimer; 25578 25579 editor.on('selectionchange', function() { 25580 if (selectionTimer) { 25581 clearTimeout(selectionTimer); 25582 selectionTimer = 0; 25583 } 25584 25585 selectionTimer = window.setTimeout(function() { 25586 if (editor.removed) { 25587 return; 25588 } 25589 25590 var rng = selection.getRng(); 25591 25592 // Compare the ranges to see if it was a real change or not 25593 if (!lastRng || !RangeUtils.compareRanges(rng, lastRng)) { 25594 editor.nodeChanged(); 25595 lastRng = rng; 25596 } 25597 }, 50); 25598 }); 25599 } 25600 25601 /** 25602 * Screen readers on IE needs to have the role application set on the body. 25603 */ 25604 function ensureBodyHasRoleApplication() { 25605 document.body.setAttribute("role", "application"); 25606 } 25607 25608 /** 25609 * Backspacing into a table behaves differently depending upon browser type. 25610 * Therefore, disable Backspace when cursor immediately follows a table. 25611 */ 25612 function disableBackspaceIntoATable() { 25613 editor.on('keydown', function(e) { 25614 if (!isDefaultPrevented(e) && e.keyCode === BACKSPACE) { 25615 if (selection.isCollapsed() && selection.getRng(true).startOffset === 0) { 25616 var previousSibling = selection.getNode().previousSibling; 25617 if (previousSibling && previousSibling.nodeName && previousSibling.nodeName.toLowerCase() === "table") { 25618 e.preventDefault(); 25619 return false; 25620 } 25621 } 25622 } 25623 }); 25624 } 25625 25626 /** 25627 * Old IE versions can't properly render BR elements in PRE tags white in contentEditable mode. So this 25628 * logic adds a \n before the BR so that it will get rendered. 25629 */ 25630 function addNewLinesBeforeBrInPre() { 25631 // IE8+ rendering mode does the right thing with BR in PRE 25632 if (getDocumentMode() > 7) { 25633 return; 25634 } 25635 25636 // Enable display: none in area and add a specific class that hides all BR elements in PRE to 25637 // avoid the caret from getting stuck at the BR elements while pressing the right arrow key 25638 setEditorCommandState('RespectVisibilityInDesign', true); 25639 editor.contentStyles.push('.mceHideBrInPre pre br {display: none}'); 25640 dom.addClass(editor.getBody(), 'mceHideBrInPre'); 25641 25642 // Adds a \n before all BR elements in PRE to get them visual 25643 parser.addNodeFilter('pre', function(nodes) { 25644 var i = nodes.length, brNodes, j, brElm, sibling; 25645 25646 while (i--) { 25647 brNodes = nodes[i].getAll('br'); 25648 j = brNodes.length; 25649 while (j--) { 25650 brElm = brNodes[j]; 25651 25652 // Add \n before BR in PRE elements on older IE:s so the new lines get rendered 25653 sibling = brElm.prev; 25654 if (sibling && sibling.type === 3 && sibling.value.charAt(sibling.value - 1) != '\n') { 25655 sibling.value += '\n'; 25656 } else { 25657 brElm.parent.insert(new Node('#text', 3), brElm, true).value = '\n'; 25658 } 25659 } 25660 } 25661 }); 25662 25663 // Removes any \n before BR elements in PRE since other browsers and in contentEditable=false mode they will be visible 25664 serializer.addNodeFilter('pre', function(nodes) { 25665 var i = nodes.length, brNodes, j, brElm, sibling; 25666 25667 while (i--) { 25668 brNodes = nodes[i].getAll('br'); 25669 j = brNodes.length; 25670 while (j--) { 25671 brElm = brNodes[j]; 25672 sibling = brElm.prev; 25673 if (sibling && sibling.type == 3) { 25674 sibling.value = sibling.value.replace(/\r?\n$/, ''); 25675 } 25676 } 25677 } 25678 }); 25679 } 25680 25681 /** 25682 * Moves style width/height to attribute width/height when the user resizes an image on IE. 25683 */ 25684 function removePreSerializedStylesWhenSelectingControls() { 25685 dom.bind(editor.getBody(), 'mouseup', function() { 25686 var value, node = selection.getNode(); 25687 25688 // Moved styles to attributes on IMG eements 25689 if (node.nodeName == 'IMG') { 25690 // Convert style width to width attribute 25691 if ((value = dom.getStyle(node, 'width'))) { 25692 dom.setAttrib(node, 'width', value.replace(/[^0-9%]+/g, '')); 25693 dom.setStyle(node, 'width', ''); 25694 } 25695 25696 // Convert style height to height attribute 25697 if ((value = dom.getStyle(node, 'height'))) { 25698 dom.setAttrib(node, 'height', value.replace(/[^0-9%]+/g, '')); 25699 dom.setStyle(node, 'height', ''); 25700 } 25701 } 25702 }); 25703 } 25704 25705 /** 25706 * Removes a blockquote when backspace is pressed at the beginning of it. 25707 * 25708 * For example: 25709 * <blockquote><p>|x</p></blockquote> 25710 * 25711 * Becomes: 25712 * <p>|x</p> 25713 */ 25714 function removeBlockQuoteOnBackSpace() { 25715 // Add block quote deletion handler 25716 editor.on('keydown', function(e) { 25717 var rng, container, offset, root, parent; 25718 25719 if (isDefaultPrevented(e) || e.keyCode != VK.BACKSPACE) { 25720 return; 25721 } 25722 25723 rng = selection.getRng(); 25724 container = rng.startContainer; 25725 offset = rng.startOffset; 25726 root = dom.getRoot(); 25727 parent = container; 25728 25729 if (!rng.collapsed || offset !== 0) { 25730 return; 25731 } 25732 25733 while (parent && parent.parentNode && parent.parentNode.firstChild == parent && parent.parentNode != root) { 25734 parent = parent.parentNode; 25735 } 25736 25737 // Is the cursor at the beginning of a blockquote? 25738 if (parent.tagName === 'BLOCKQUOTE') { 25739 // Remove the blockquote 25740 editor.formatter.toggle('blockquote', null, parent); 25741 25742 // Move the caret to the beginning of container 25743 rng = dom.createRng(); 25744 rng.setStart(container, 0); 25745 rng.setEnd(container, 0); 25746 selection.setRng(rng); 25747 } 25748 }); 25749 } 25750 25751 /** 25752 * Sets various Gecko editing options on mouse down and before a execCommand to disable inline table editing that is broken etc. 25753 */ 25754 function setGeckoEditingOptions() { 25755 function setOpts() { 25756 editor._refreshContentEditable(); 25757 25758 setEditorCommandState("StyleWithCSS", false); 25759 setEditorCommandState("enableInlineTableEditing", false); 25760 25761 if (!settings.object_resizing) { 25762 setEditorCommandState("enableObjectResizing", false); 25763 } 25764 } 25765 25766 if (!settings.readonly) { 25767 editor.on('BeforeExecCommand MouseDown', setOpts); 25768 } 25769 } 25770 25771 /** 25772 * Fixes a gecko link bug, when a link is placed at the end of block elements there is 25773 * no way to move the caret behind the link. This fix adds a bogus br element after the link. 25774 * 25775 * For example this: 25776 * <p><b><a href="#">x</a></b></p> 25777 * 25778 * Becomes this: 25779 * <p><b><a href="#">x</a></b><br></p> 25780 */ 25781 function addBrAfterLastLinks() { 25782 function fixLinks() { 25783 each(dom.select('a'), function(node) { 25784 var parentNode = node.parentNode, root = dom.getRoot(); 25785 25786 if (parentNode.lastChild === node) { 25787 while (parentNode && !dom.isBlock(parentNode)) { 25788 if (parentNode.parentNode.lastChild !== parentNode || parentNode === root) { 25789 return; 25790 } 25791 25792 parentNode = parentNode.parentNode; 25793 } 25794 25795 dom.add(parentNode, 'br', {'data-mce-bogus': 1}); 25796 } 25797 }); 25798 } 25799 25800 editor.on('SetContent ExecCommand', function(e) { 25801 if (e.type == "setcontent" || e.command === 'mceInsertLink') { 25802 fixLinks(); 25803 } 25804 }); 25805 } 25806 25807 /** 25808 * WebKit will produce DIV elements here and there by default. But since TinyMCE uses paragraphs by 25809 * default we want to change that behavior. 25810 */ 25811 function setDefaultBlockType() { 25812 if (settings.forced_root_block) { 25813 editor.on('init', function() { 25814 setEditorCommandState('DefaultParagraphSeparator', settings.forced_root_block); 25815 }); 25816 } 25817 } 25818 25819 /** 25820 * Removes ghost selections from images/tables on Gecko. 25821 */ 25822 function removeGhostSelection() { 25823 editor.on('Undo Redo SetContent', function(e) { 25824 if (!e.initial) { 25825 editor.execCommand('mceRepaint'); 25826 } 25827 }); 25828 } 25829 25830 /** 25831 * Deletes the selected image on IE instead of navigating to previous page. 25832 */ 25833 function deleteControlItemOnBackSpace() { 25834 editor.on('keydown', function(e) { 25835 var rng; 25836 25837 if (!isDefaultPrevented(e) && e.keyCode == BACKSPACE) { 25838 rng = editor.getDoc().selection.createRange(); 25839 if (rng && rng.item) { 25840 e.preventDefault(); 25841 editor.undoManager.beforeChange(); 25842 dom.remove(rng.item(0)); 25843 editor.undoManager.add(); 25844 } 25845 } 25846 }); 25847 } 25848 25849 /** 25850 * IE10 doesn't properly render block elements with the right height until you add contents to them. 25851 * This fixes that by adding a padding-right to all empty text block elements. 25852 * See: https://connect.microsoft.com/IE/feedback/details/743881 25853 */ 25854 function renderEmptyBlocksFix() { 25855 var emptyBlocksCSS; 25856 25857 // IE10+ 25858 if (getDocumentMode() >= 10) { 25859 emptyBlocksCSS = ''; 25860 each('p div h1 h2 h3 h4 h5 h6'.split(' '), function(name, i) { 25861 emptyBlocksCSS += (i > 0 ? ',' : '') + name + ':empty'; 25862 }); 25863 25864 editor.contentStyles.push(emptyBlocksCSS + '{padding-right: 1px !important}'); 25865 } 25866 } 25867 25868 /** 25869 * Old IE versions can't retain contents within noscript elements so this logic will store the contents 25870 * as a attribute and the insert that value as it's raw text when the DOM is serialized. 25871 */ 25872 function keepNoScriptContents() { 25873 if (getDocumentMode() < 9) { 25874 parser.addNodeFilter('noscript', function(nodes) { 25875 var i = nodes.length, node, textNode; 25876 25877 while (i--) { 25878 node = nodes[i]; 25879 textNode = node.firstChild; 25880 25881 if (textNode) { 25882 node.attr('data-mce-innertext', textNode.value); 25883 } 25884 } 25885 }); 25886 25887 serializer.addNodeFilter('noscript', function(nodes) { 25888 var i = nodes.length, node, textNode, value; 25889 25890 while (i--) { 25891 node = nodes[i]; 25892 textNode = nodes[i].firstChild; 25893 25894 if (textNode) { 25895 textNode.value = Entities.decode(textNode.value); 25896 } else { 25897 // Old IE can't retain noscript value so an attribute is used to store it 25898 value = node.attributes.map['data-mce-innertext']; 25899 if (value) { 25900 node.attr('data-mce-innertext', null); 25901 textNode = new Node('#text', 3); 25902 textNode.value = value; 25903 textNode.raw = true; 25904 node.append(textNode); 25905 } 25906 } 25907 } 25908 }); 25909 } 25910 } 25911 25912 /** 25913 * IE has an issue where you can't select/move the caret by clicking outside the body if the document is in standards mode. 25914 */ 25915 function fixCaretSelectionOfDocumentElementOnIe() { 25916 var doc = dom.doc, body = doc.body, started, startRng, htmlElm; 25917 25918 // Return range from point or null if it failed 25919 function rngFromPoint(x, y) { 25920 var rng = body.createTextRange(); 25921 25922 try { 25923 rng.moveToPoint(x, y); 25924 } catch (ex) { 25925 // IE sometimes throws and exception, so lets just ignore it 25926 rng = null; 25927 } 25928 25929 return rng; 25930 } 25931 25932 // Fires while the selection is changing 25933 function selectionChange(e) { 25934 var pointRng; 25935 25936 // Check if the button is down or not 25937 if (e.button) { 25938 // Create range from mouse position 25939 pointRng = rngFromPoint(e.x, e.y); 25940 25941 if (pointRng) { 25942 // Check if pointRange is before/after selection then change the endPoint 25943 if (pointRng.compareEndPoints('StartToStart', startRng) > 0) { 25944 pointRng.setEndPoint('StartToStart', startRng); 25945 } else { 25946 pointRng.setEndPoint('EndToEnd', startRng); 25947 } 25948 25949 pointRng.select(); 25950 } 25951 } else { 25952 endSelection(); 25953 } 25954 } 25955 25956 // Removes listeners 25957 function endSelection() { 25958 var rng = doc.selection.createRange(); 25959 25960 // If the range is collapsed then use the last start range 25961 if (startRng && !rng.item && rng.compareEndPoints('StartToEnd', rng) === 0) { 25962 startRng.select(); 25963 } 25964 25965 dom.unbind(doc, 'mouseup', endSelection); 25966 dom.unbind(doc, 'mousemove', selectionChange); 25967 startRng = started = 0; 25968 } 25969 25970 // Make HTML element unselectable since we are going to handle selection by hand 25971 doc.documentElement.unselectable = true; 25972 25973 // Detect when user selects outside BODY 25974 dom.bind(doc, 'mousedown contextmenu', function(e) { 25975 if (e.target.nodeName === 'HTML') { 25976 if (started) { 25977 endSelection(); 25978 } 25979 25980 // Detect vertical scrollbar, since IE will fire a mousedown on the scrollbar and have target set as HTML 25981 htmlElm = doc.documentElement; 25982 if (htmlElm.scrollHeight > htmlElm.clientHeight) { 25983 return; 25984 } 25985 25986 started = 1; 25987 // Setup start position 25988 startRng = rngFromPoint(e.x, e.y); 25989 if (startRng) { 25990 // Listen for selection change events 25991 dom.bind(doc, 'mouseup', endSelection); 25992 dom.bind(doc, 'mousemove', selectionChange); 25993 25994 dom.getRoot().focus(); 25995 startRng.select(); 25996 } 25997 } 25998 }); 25999 } 26000 26001 /** 26002 * Fixes selection issues where the caret can be placed between two inline elements like <b>a</b>|<b>b</b> 26003 * this fix will lean the caret right into the closest inline element. 26004 */ 26005 function normalizeSelection() { 26006 // Normalize selection for example <b>a</b><i>|a</i> becomes <b>a|</b><i>a</i> except for Ctrl+A since it selects everything 26007 editor.on('keyup focusin mouseup', function(e) { 26008 if (e.keyCode != 65 || !VK.metaKeyPressed(e)) { 26009 selection.normalize(); 26010 } 26011 }, true); 26012 } 26013 26014 /** 26015 * Forces Gecko to render a broken image icon if it fails to load an image. 26016 */ 26017 function showBrokenImageIcon() { 26018 editor.contentStyles.push( 26019 'img:-moz-broken {' + 26020 '-moz-force-broken-image-icon:1;' + 26021 'min-width:24px;' + 26022 'min-height:24px' + 26023 '}' 26024 ); 26025 } 26026 26027 /** 26028 * iOS has a bug where it's impossible to type if the document has a touchstart event 26029 * bound and the user touches the document while having the on screen keyboard visible. 26030 * 26031 * The touch event moves the focus to the parent document while having the caret inside the iframe 26032 * this fix moves the focus back into the iframe document. 26033 */ 26034 function restoreFocusOnKeyDown() { 26035 if (!editor.inline) { 26036 editor.on('keydown', function() { 26037 if (document.activeElement == document.body) { 26038 editor.getWin().focus(); 26039 } 26040 }); 26041 } 26042 } 26043 26044 /** 26045 * IE 11 has an annoying issue where you can't move focus into the editor 26046 * by clicking on the white area HTML element. We used to be able to to fix this with 26047 * the fixCaretSelectionOfDocumentElementOnIe fix. But since M$ removed the selection 26048 * object it's not possible anymore. So we need to hack in a ungly CSS to force the 26049 * body to be at least 150px. If the user clicks the HTML element out side this 150px region 26050 * we simply move the focus into the first paragraph. Not ideal since you loose the 26051 * positioning of the caret but goot enough for most cases. 26052 */ 26053 function bodyHeight() { 26054 if (!editor.inline) { 26055 editor.contentStyles.push('body {min-height: 150px}'); 26056 editor.on('click', function(e) { 26057 if (e.target.nodeName == 'HTML') { 26058 editor.getBody().focus(); 26059 editor.selection.normalize(); 26060 editor.nodeChanged(); 26061 } 26062 }); 26063 } 26064 } 26065 26066 /** 26067 * Firefox on Mac OS will move the browser back to the previous page if you press CMD+Left arrow. 26068 * You might then loose all your work so we need to block that behavior and replace it with our own. 26069 */ 26070 function blockCmdArrowNavigation() { 26071 if (Env.mac) { 26072 editor.on('keydown', function(e) { 26073 if (VK.metaKeyPressed(e) && (e.keyCode == 37 || e.keyCode == 39)) { 26074 e.preventDefault(); 26075 editor.selection.getSel().modify('move', e.keyCode == 37 ? 'backward' : 'forward', 'word'); 26076 } 26077 }); 26078 } 26079 } 26080 26081 /** 26082 * Disables the autolinking in IE 9+ this is then re-enabled by the autolink plugin. 26083 */ 26084 function disableAutoUrlDetect() { 26085 setEditorCommandState("AutoUrlDetect", false); 26086 } 26087 26088 /** 26089 * IE 11 has a fantastic bug where it will produce two trailing BR elements to iframe bodies when 26090 * the iframe is hidden by display: none on a parent container. The DOM is actually out of sync 26091 * with innerHTML in this case. It's like IE adds shadow DOM BR elements that appears on innerHTML 26092 * but not as the lastChild of the body. However is we add a BR element to the body then remove it 26093 * it doesn't seem to add these BR elements makes sence right?! 26094 * 26095 * Example of what happens: <body>text</body> becomes <body>text<br><br></body> 26096 */ 26097 function doubleTrailingBrElements() { 26098 if (!editor.inline) { 26099 editor.on('focus blur beforegetcontent', function() { 26100 var br = editor.dom.create('br'); 26101 editor.getBody().appendChild(br); 26102 br.parentNode.removeChild(br); 26103 }, true); 26104 } 26105 } 26106 26107 /** 26108 * iOS 7.1 introduced two new bugs: 26109 * 1) It's possible to open links within a contentEditable area by clicking on them. 26110 * 2) If you hold down the finger it will display the link/image touch callout menu. 26111 */ 26112 function tapLinksAndImages() { 26113 editor.on('click', function(e) { 26114 var elm = e.target; 26115 26116 do { 26117 if (elm.tagName === 'A') { 26118 e.preventDefault(); 26119 return; 26120 } 26121 } while ((elm = elm.parentNode)); 26122 }); 26123 26124 editor.contentStyles.push('.mce-content-body {-webkit-touch-callout: none}'); 26125 } 26126 26127 /** 26128 * WebKit has a bug where it will allow forms to be submitted if they are inside a contentEditable element. 26129 * For example this: <form><button></form> 26130 */ 26131 function blockFormSubmitInsideEditor() { 26132 editor.on('init', function() { 26133 editor.dom.bind(editor.getBody(), 'submit', function(e) { 26134 e.preventDefault(); 26135 }); 26136 }); 26137 } 26138 26139 // All browsers 26140 disableBackspaceIntoATable(); 26141 removeBlockQuoteOnBackSpace(); 26142 emptyEditorWhenDeleting(); 26143 normalizeSelection(); 26144 26145 // WebKit 26146 if (isWebKit) { 26147 cleanupStylesWhenDeleting(); 26148 inputMethodFocus(); 26149 selectControlElements(); 26150 setDefaultBlockType(); 26151 blockFormSubmitInsideEditor(); 26152 26153 // iOS 26154 if (Env.iOS) { 26155 selectionChangeNodeChanged(); 26156 restoreFocusOnKeyDown(); 26157 bodyHeight(); 26158 tapLinksAndImages(); 26159 } else { 26160 selectAll(); 26161 } 26162 } 26163 26164 // IE 26165 if (isIE && Env.ie < 11) { 26166 removeHrOnBackspace(); 26167 ensureBodyHasRoleApplication(); 26168 addNewLinesBeforeBrInPre(); 26169 removePreSerializedStylesWhenSelectingControls(); 26170 deleteControlItemOnBackSpace(); 26171 renderEmptyBlocksFix(); 26172 keepNoScriptContents(); 26173 fixCaretSelectionOfDocumentElementOnIe(); 26174 } 26175 26176 if (Env.ie >= 11) { 26177 bodyHeight(); 26178 doubleTrailingBrElements(); 26179 } 26180 26181 if (Env.ie) { 26182 selectAll(); 26183 disableAutoUrlDetect(); 26184 } 26185 26186 // Gecko 26187 if (isGecko) { 26188 removeHrOnBackspace(); 26189 focusBody(); 26190 removeStylesWhenDeletingAcrossBlockElements(); 26191 setGeckoEditingOptions(); 26192 addBrAfterLastLinks(); 26193 removeGhostSelection(); 26194 showBrokenImageIcon(); 26195 blockCmdArrowNavigation(); 26196 } 26197 }; 26198 }); 26199 26200 // Included from: js/tinymce/classes/util/Observable.js 26201 26202 /** 26203 * Observable.js 26204 * 26205 * Copyright, Moxiecode Systems AB 26206 * Released under LGPL License. 26207 * 26208 * License: http://www.tinymce.com/license 26209 * Contributing: http://www.tinymce.com/contributing 26210 */ 26211 26212 /** 26213 * This mixin will add event binding logic to classes. 26214 * 26215 * @mixin tinymce.util.Observable 26216 */ 26217 define("tinymce/util/Observable", [ 26218 "tinymce/util/EventDispatcher" 26219 ], function(EventDispatcher) { 26220 function getEventDispatcher(obj) { 26221 if (!obj._eventDispatcher) { 26222 obj._eventDispatcher = new EventDispatcher({ 26223 scope: obj, 26224 toggleEvent: function(name, state) { 26225 if (EventDispatcher.isNative(name) && obj.toggleNativeEvent) { 26226 obj.toggleNativeEvent(name, state); 26227 } 26228 } 26229 }); 26230 } 26231 26232 return obj._eventDispatcher; 26233 } 26234 26235 return { 26236 /** 26237 * Fires the specified event by name. 26238 * 26239 * @method fire 26240 * @param {String} name Name of the event to fire. 26241 * @param {Object?} args Event arguments. 26242 * @param {Boolean?} bubble True/false if the event is to be bubbled. 26243 * @return {Object} Event args instance passed in. 26244 * @example 26245 * instance.fire('event', {...}); 26246 */ 26247 fire: function(name, args, bubble) { 26248 var self = this; 26249 26250 // Prevent all events except the remove event after the instance has been removed 26251 if (self.removed && name !== "remove") { 26252 return args; 26253 } 26254 26255 args = getEventDispatcher(self).fire(name, args, bubble); 26256 26257 // Bubble event up to parents 26258 if (bubble !== false && self.parent) { 26259 var parent = self.parent(); 26260 while (parent && !args.isPropagationStopped()) { 26261 parent.fire(name, args, false); 26262 parent = parent.parent(); 26263 } 26264 } 26265 26266 return args; 26267 }, 26268 26269 /** 26270 * Binds an event listener to a specific event by name. 26271 * 26272 * @method on 26273 * @param {String} name Event name or space separated list of events to bind. 26274 * @param {callback} callback Callback to be executed when the event occurs. 26275 * @param {Boolean} first Optional flag if the event should be prepended. Use this with care. 26276 * @return {Object} Current class instance. 26277 * @example 26278 * instance.on('event', function(e) { 26279 * // Callback logic 26280 * }); 26281 */ 26282 on: function(name, callback, prepend) { 26283 return getEventDispatcher(this).on(name, callback, prepend); 26284 }, 26285 26286 /** 26287 * Unbinds an event listener to a specific event by name. 26288 * 26289 * @method off 26290 * @param {String?} name Name of the event to unbind. 26291 * @param {callback?} callback Callback to unbind. 26292 * @return {Object} Current class instance. 26293 * @example 26294 * // Unbind specific callback 26295 * instance.off('event', handler); 26296 * 26297 * // Unbind all listeners by name 26298 * instance.off('event'); 26299 * 26300 * // Unbind all events 26301 * instance.off(); 26302 */ 26303 off: function(name, callback) { 26304 return getEventDispatcher(this).off(name, callback); 26305 }, 26306 26307 /** 26308 * Returns true/false if the object has a event of the specified name. 26309 * 26310 * @method hasEventListeners 26311 * @param {String} name Name of the event to check for. 26312 * @return {Boolean} true/false if the event exists or not. 26313 */ 26314 hasEventListeners: function(name) { 26315 return getEventDispatcher(this).has(name); 26316 } 26317 }; 26318 }); 26319 26320 // Included from: js/tinymce/classes/EditorObservable.js 26321 26322 /** 26323 * EditorObservable.js 26324 * 26325 * Copyright, Moxiecode Systems AB 26326 * Released under LGPL License. 26327 * 26328 * License: http://www.tinymce.com/license 26329 * Contributing: http://www.tinymce.com/contributing 26330 */ 26331 26332 /** 26333 * This mixin contains the event logic for the tinymce.Editor class. 26334 * 26335 * @mixin tinymce.EditorObservable 26336 * @extends tinymce.util.Observable 26337 */ 26338 define("tinymce/EditorObservable", [ 26339 "tinymce/util/Observable", 26340 "tinymce/dom/DOMUtils", 26341 "tinymce/util/Tools" 26342 ], function(Observable, DOMUtils, Tools) { 26343 var DOM = DOMUtils.DOM; 26344 26345 function getEventTarget(editor, eventName) { 26346 if (eventName == 'selectionchange') { 26347 return editor.getDoc(); 26348 } 26349 26350 // Need to bind mousedown/mouseup etc to document not body in iframe mode 26351 // Since the user might click on the HTML element not the BODY 26352 if (!editor.inline && /^mouse|click|contextmenu|drop/.test(eventName)) { 26353 return editor.getDoc(); 26354 } 26355 26356 return editor.getBody(); 26357 } 26358 26359 function bindEventDelegate(editor, name) { 26360 var eventRootSelector = editor.settings.event_root, editorManager = editor.editorManager; 26361 var eventRootElm = editorManager.eventRootElm || getEventTarget(editor, name); 26362 26363 if (eventRootSelector) { 26364 if (!editorManager.rootEvents) { 26365 editorManager.rootEvents = {}; 26366 26367 editorManager.on('RemoveEditor', function() { 26368 if (!editorManager.activeEditor) { 26369 DOM.unbind(eventRootElm); 26370 delete editorManager.rootEvents; 26371 } 26372 }); 26373 } 26374 26375 if (editorManager.rootEvents[name]) { 26376 return; 26377 } 26378 26379 if (eventRootElm == editor.getBody()) { 26380 eventRootElm = DOM.select(eventRootSelector)[0]; 26381 editorManager.eventRootElm = eventRootElm; 26382 } 26383 26384 editorManager.rootEvents[name] = true; 26385 26386 DOM.bind(eventRootElm, name, function(e) { 26387 var target = e.target, editors = editorManager.editors, i = editors.length; 26388 26389 while (i--) { 26390 var body = editors[i].getBody(); 26391 26392 if (body === target || DOM.isChildOf(target, body)) { 26393 if (!editors[i].hidden) { 26394 editors[i].fire(name, e); 26395 } 26396 } 26397 } 26398 }); 26399 } else { 26400 editor.dom.bind(eventRootElm, name, function(e) { 26401 if (!editor.hidden) { 26402 editor.fire(name, e); 26403 } 26404 }); 26405 } 26406 } 26407 26408 var EditorObservable = { 26409 bindPendingEventDelegates: function() { 26410 var self = this; 26411 26412 Tools.each(self._pendingNativeEvents, function(name) { 26413 bindEventDelegate(self, name); 26414 }); 26415 }, 26416 26417 toggleNativeEvent: function(name, state) { 26418 var self = this; 26419 26420 if (self.settings.readonly) { 26421 return; 26422 } 26423 26424 // Never bind focus/blur since the FocusManager fakes those 26425 if (name == "focus" || name == "blur") { 26426 return; 26427 } 26428 26429 if (state) { 26430 if (self.initialized) { 26431 bindEventDelegate(self, name); 26432 } else { 26433 if (!self._pendingNativeEvents) { 26434 self._pendingNativeEvents = [name]; 26435 } else { 26436 self._pendingNativeEvents.push(name); 26437 } 26438 } 26439 } else if (self.initialized) { 26440 self.dom.unbind(getEventTarget(self, name), name); 26441 } 26442 } 26443 }; 26444 26445 EditorObservable = Tools.extend({}, Observable, EditorObservable); 26446 26447 return EditorObservable; 26448 }); 26449 26450 // Included from: js/tinymce/classes/Shortcuts.js 26451 26452 /** 26453 * Shortcuts.js 26454 * 26455 * Copyright, Moxiecode Systems AB 26456 * Released under LGPL License. 26457 * 26458 * License: http://www.tinymce.com/license 26459 * Contributing: http://www.tinymce.com/contributing 26460 */ 26461 26462 /** 26463 * Contains all logic for handling of keyboard shortcuts. 26464 */ 26465 define("tinymce/Shortcuts", [ 26466 "tinymce/util/Tools", 26467 "tinymce/Env" 26468 ], function(Tools, Env) { 26469 var each = Tools.each, explode = Tools.explode; 26470 26471 var keyCodeLookup = { 26472 "f9": 120, 26473 "f10": 121, 26474 "f11": 122 26475 }; 26476 26477 return function(editor) { 26478 var self = this, shortcuts = {}; 26479 26480 editor.on('keyup keypress keydown', function(e) { 26481 if (e.altKey || e.ctrlKey || e.metaKey) { 26482 each(shortcuts, function(shortcut) { 26483 var ctrlKey = Env.mac ? e.metaKey : e.ctrlKey; 26484 26485 if (shortcut.ctrl != ctrlKey || shortcut.alt != e.altKey || shortcut.shift != e.shiftKey) { 26486 return; 26487 } 26488 26489 if (e.keyCode == shortcut.keyCode || (e.charCode && e.charCode == shortcut.charCode)) { 26490 e.preventDefault(); 26491 26492 if (e.type == "keydown") { 26493 shortcut.func.call(shortcut.scope); 26494 } 26495 26496 return true; 26497 } 26498 }); 26499 } 26500 }); 26501 26502 /** 26503 * Adds a keyboard shortcut for some command or function. 26504 * 26505 * @method addShortcut 26506 * @param {String} pattern Shortcut pattern. Like for example: ctrl+alt+o. 26507 * @param {String} desc Text description for the command. 26508 * @param {String/Function} cmdFunc Command name string or function to execute when the key is pressed. 26509 * @param {Object} sc Optional scope to execute the function in. 26510 * @return {Boolean} true/false state if the shortcut was added or not. 26511 */ 26512 self.add = function(pattern, desc, cmdFunc, scope) { 26513 var cmd; 26514 26515 cmd = cmdFunc; 26516 26517 if (typeof(cmdFunc) === 'string') { 26518 cmdFunc = function() { 26519 editor.execCommand(cmd, false, null); 26520 }; 26521 } else if (Tools.isArray(cmd)) { 26522 cmdFunc = function() { 26523 editor.execCommand(cmd[0], cmd[1], cmd[2]); 26524 }; 26525 } 26526 26527 each(explode(pattern.toLowerCase()), function(pattern) { 26528 var shortcut = { 26529 func: cmdFunc, 26530 scope: scope || editor, 26531 desc: editor.translate(desc), 26532 alt: false, 26533 ctrl: false, 26534 shift: false 26535 }; 26536 26537 each(explode(pattern, '+'), function(value) { 26538 switch (value) { 26539 case 'alt': 26540 case 'ctrl': 26541 case 'shift': 26542 shortcut[value] = true; 26543 break; 26544 26545 default: 26546 shortcut.charCode = value.charCodeAt(0); 26547 shortcut.keyCode = keyCodeLookup[value] || value.toUpperCase().charCodeAt(0); 26548 } 26549 }); 26550 26551 shortcuts[ 26552 (shortcut.ctrl ? 'ctrl' : '') + ',' + 26553 (shortcut.alt ? 'alt' : '') + ',' + 26554 (shortcut.shift ? 'shift' : '') + ',' + 26555 shortcut.keyCode 26556 ] = shortcut; 26557 }); 26558 26559 return true; 26560 }; 26561 }; 26562 }); 26563 26564 // Included from: js/tinymce/classes/Editor.js 26565 26566 /** 26567 * Editor.js 26568 * 26569 * Copyright, Moxiecode Systems AB 26570 * Released under LGPL License. 26571 * 26572 * License: http://www.tinymce.com/license 26573 * Contributing: http://www.tinymce.com/contributing 26574 */ 26575 26576 /*jshint scripturl:true */ 26577 26578 /** 26579 * Include the base event class documentation. 26580 * 26581 * @include ../../../tools/docs/tinymce.Event.js 26582 */ 26583 26584 /** 26585 * This class contains the core logic for a TinyMCE editor. 26586 * 26587 * @class tinymce.Editor 26588 * @mixes tinymce.util.Observable 26589 * @example 26590 * // Add a class to all paragraphs in the editor. 26591 * tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('p'), 'someclass'); 26592 * 26593 * // Gets the current editors selection as text 26594 * tinymce.activeEditor.selection.getContent({format: 'text'}); 26595 * 26596 * // Creates a new editor instance 26597 * var ed = new tinymce.Editor('textareaid', { 26598 * some_setting: 1 26599 * }, tinymce.EditorManager); 26600 * 26601 * // Select each item the user clicks on 26602 * ed.on('click', function(e) { 26603 * ed.selection.select(e.target); 26604 * }); 26605 * 26606 * ed.render(); 26607 */ 26608 define("tinymce/Editor", [ 26609 "tinymce/dom/DOMUtils", 26610 "tinymce/AddOnManager", 26611 "tinymce/html/Node", 26612 "tinymce/dom/Serializer", 26613 "tinymce/html/Serializer", 26614 "tinymce/dom/Selection", 26615 "tinymce/Formatter", 26616 "tinymce/UndoManager", 26617 "tinymce/EnterKey", 26618 "tinymce/ForceBlocks", 26619 "tinymce/EditorCommands", 26620 "tinymce/util/URI", 26621 "tinymce/dom/ScriptLoader", 26622 "tinymce/dom/EventUtils", 26623 "tinymce/WindowManager", 26624 "tinymce/html/Schema", 26625 "tinymce/html/DomParser", 26626 "tinymce/util/Quirks", 26627 "tinymce/Env", 26628 "tinymce/util/Tools", 26629 "tinymce/EditorObservable", 26630 "tinymce/Shortcuts" 26631 ], function( 26632 DOMUtils, AddOnManager, Node, DomSerializer, Serializer, 26633 Selection, Formatter, UndoManager, EnterKey, ForceBlocks, EditorCommands, 26634 URI, ScriptLoader, EventUtils, WindowManager, 26635 Schema, DomParser, Quirks, Env, Tools, EditorObservable, Shortcuts 26636 ) { 26637 // Shorten these names 26638 var DOM = DOMUtils.DOM, ThemeManager = AddOnManager.ThemeManager, PluginManager = AddOnManager.PluginManager; 26639 var extend = Tools.extend, each = Tools.each, explode = Tools.explode; 26640 var inArray = Tools.inArray, trim = Tools.trim, resolve = Tools.resolve; 26641 var Event = EventUtils.Event; 26642 var isGecko = Env.gecko, ie = Env.ie; 26643 26644 /** 26645 * Include documentation for all the events. 26646 * 26647 * @include ../../../tools/docs/tinymce.Editor.js 26648 */ 26649 26650 /** 26651 * Constructs a editor instance by id. 26652 * 26653 * @constructor 26654 * @method Editor 26655 * @param {String} id Unique id for the editor. 26656 * @param {Object} settings Settings for the editor. 26657 * @param {tinymce.EditorManager} editorManager EditorManager instance. 26658 * @author Moxiecode 26659 */ 26660 function Editor(id, settings, editorManager) { 26661 var self = this, documentBaseUrl, baseUri; 26662 26663 documentBaseUrl = self.documentBaseUrl = editorManager.documentBaseURL; 26664 baseUri = editorManager.baseURI; 26665 26666 /** 26667 * Name/value collection with editor settings. 26668 * 26669 * @property settings 26670 * @type Object 26671 * @example 26672 * // Get the value of the theme setting 26673 * tinymce.activeEditor.windowManager.alert("You are using the " + tinymce.activeEditor.settings.theme + " theme"); 26674 */ 26675 self.settings = settings = extend({ 26676 id: id, 26677 theme: 'modern', 26678 delta_width: 0, 26679 delta_height: 0, 26680 popup_css: '', 26681 plugins: '', 26682 document_base_url: documentBaseUrl, 26683 add_form_submit_trigger: true, 26684 submit_patch: true, 26685 add_unload_trigger: true, 26686 convert_urls: true, 26687 relative_urls: true, 26688 remove_script_host: true, 26689 object_resizing: true, 26690 doctype: '<!DOCTYPE html>', 26691 visual: true, 26692 font_size_style_values: 'xx-small,x-small,small,medium,large,x-large,xx-large', 26693 26694 // See: http://www.w3.org/TR/CSS2/fonts.html#propdef-font-size 26695 font_size_legacy_values: 'xx-small,small,medium,large,x-large,xx-large,300%', 26696 forced_root_block: 'p', 26697 hidden_input: true, 26698 padd_empty_editor: true, 26699 render_ui: true, 26700 indentation: '30px', 26701 inline_styles: true, 26702 convert_fonts_to_spans: true, 26703 indent: 'simple', 26704 indent_before: 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,ul,li,area,table,thead,' + 26705 'tfoot,tbody,tr,section,article,hgroup,aside,figure,option,optgroup,datalist', 26706 indent_after: 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,ul,li,area,table,thead,' + 26707 'tfoot,tbody,tr,section,article,hgroup,aside,figure,option,optgroup,datalist', 26708 validate: true, 26709 entity_encoding: 'named', 26710 url_converter: self.convertURL, 26711 url_converter_scope: self, 26712 ie7_compat: true 26713 }, settings); 26714 26715 AddOnManager.language = settings.language || 'en'; 26716 AddOnManager.languageLoad = settings.language_load; 26717 26718 AddOnManager.baseURL = editorManager.baseURL; 26719 26720 /** 26721 * Editor instance id, normally the same as the div/textarea that was replaced. 26722 * 26723 * @property id 26724 * @type String 26725 */ 26726 self.id = settings.id = id; 26727 26728 /** 26729 * State to force the editor to return false on a isDirty call. 26730 * 26731 * @property isNotDirty 26732 * @type Boolean 26733 * @example 26734 * function ajaxSave() { 26735 * var ed = tinymce.get('elm1'); 26736 * 26737 * // Save contents using some XHR call 26738 * alert(ed.getContent()); 26739 * 26740 * ed.isNotDirty = true; // Force not dirty state 26741 * } 26742 */ 26743 self.isNotDirty = true; 26744 26745 /** 26746 * Name/Value object containting plugin instances. 26747 * 26748 * @property plugins 26749 * @type Object 26750 * @example 26751 * // Execute a method inside a plugin directly 26752 * tinymce.activeEditor.plugins.someplugin.someMethod(); 26753 */ 26754 self.plugins = {}; 26755 26756 /** 26757 * URI object to document configured for the TinyMCE instance. 26758 * 26759 * @property documentBaseURI 26760 * @type tinymce.util.URI 26761 * @example 26762 * // Get relative URL from the location of document_base_url 26763 * tinymce.activeEditor.documentBaseURI.toRelative('/somedir/somefile.htm'); 26764 * 26765 * // Get absolute URL from the location of document_base_url 26766 * tinymce.activeEditor.documentBaseURI.toAbsolute('somefile.htm'); 26767 */ 26768 self.documentBaseURI = new URI(settings.document_base_url || documentBaseUrl, { 26769 base_uri: baseUri 26770 }); 26771 26772 /** 26773 * URI object to current document that holds the TinyMCE editor instance. 26774 * 26775 * @property baseURI 26776 * @type tinymce.util.URI 26777 * @example 26778 * // Get relative URL from the location of the API 26779 * tinymce.activeEditor.baseURI.toRelative('/somedir/somefile.htm'); 26780 * 26781 * // Get absolute URL from the location of the API 26782 * tinymce.activeEditor.baseURI.toAbsolute('somefile.htm'); 26783 */ 26784 self.baseURI = baseUri; 26785 26786 /** 26787 * Array with CSS files to load into the iframe. 26788 * 26789 * @property contentCSS 26790 * @type Array 26791 */ 26792 self.contentCSS = []; 26793 26794 /** 26795 * Array of CSS styles to add to head of document when the editor loads. 26796 * 26797 * @property contentStyles 26798 * @type Array 26799 */ 26800 self.contentStyles = []; 26801 26802 // Creates all events like onClick, onSetContent etc see Editor.Events.js for the actual logic 26803 self.shortcuts = new Shortcuts(self); 26804 26805 // Internal command handler objects 26806 self.execCommands = {}; 26807 self.queryStateCommands = {}; 26808 self.queryValueCommands = {}; 26809 self.loadedCSS = {}; 26810 26811 self.suffix = editorManager.suffix; 26812 self.editorManager = editorManager; 26813 self.inline = settings.inline; 26814 26815 // Call setup 26816 editorManager.fire('SetupEditor', self); 26817 self.execCallback('setup', self); 26818 } 26819 26820 Editor.prototype = { 26821 /** 26822 * Renderes the editor/adds it to the page. 26823 * 26824 * @method render 26825 */ 26826 render: function() { 26827 var self = this, settings = self.settings, id = self.id, suffix = self.suffix; 26828 26829 function readyHandler() { 26830 DOM.unbind(window, 'ready', readyHandler); 26831 self.render(); 26832 } 26833 26834 // Page is not loaded yet, wait for it 26835 if (!Event.domLoaded) { 26836 DOM.bind(window, 'ready', readyHandler); 26837 return; 26838 } 26839 26840 // Element not found, then skip initialization 26841 if (!self.getElement()) { 26842 return; 26843 } 26844 26845 // No editable support old iOS versions etc 26846 if (!Env.contentEditable) { 26847 return; 26848 } 26849 26850 // Hide target element early to prevent content flashing 26851 if (!settings.inline) { 26852 self.orgVisibility = self.getElement().style.visibility; 26853 self.getElement().style.visibility = 'hidden'; 26854 } else { 26855 self.inline = true; 26856 } 26857 26858 var form = self.getElement().form || DOM.getParent(id, 'form'); 26859 if (form) { 26860 self.formElement = form; 26861 26862 // Add hidden input for non input elements inside form elements 26863 if (settings.hidden_input && !/TEXTAREA|INPUT/i.test(self.getElement().nodeName)) { 26864 DOM.insertAfter(DOM.create('input', {type: 'hidden', name: id}), id); 26865 self.hasHiddenInput = true; 26866 } 26867 26868 // Pass submit/reset from form to editor instance 26869 self.formEventDelegate = function(e) { 26870 self.fire(e.type, e); 26871 }; 26872 26873 DOM.bind(form, 'submit reset', self.formEventDelegate); 26874 26875 // Reset contents in editor when the form is reset 26876 self.on('reset', function() { 26877 self.setContent(self.startContent, {format: 'raw'}); 26878 }); 26879 26880 // Check page uses id="submit" or name="submit" for it's submit button 26881 if (settings.submit_patch && !form.submit.nodeType && !form.submit.length && !form._mceOldSubmit) { 26882 form._mceOldSubmit = form.submit; 26883 form.submit = function() { 26884 self.editorManager.triggerSave(); 26885 self.isNotDirty = true; 26886 26887 return form._mceOldSubmit(form); 26888 }; 26889 } 26890 } 26891 26892 /** 26893 * Window manager reference, use this to open new windows and dialogs. 26894 * 26895 * @property windowManager 26896 * @type tinymce.WindowManager 26897 * @example 26898 * // Shows an alert message 26899 * tinymce.activeEditor.windowManager.alert('Hello world!'); 26900 * 26901 * // Opens a new dialog with the file.htm file and the size 320x240 26902 * // It also adds a custom parameter this can be retrieved by using tinyMCEPopup.getWindowArg inside the dialog. 26903 * tinymce.activeEditor.windowManager.open({ 26904 * url: 'file.htm', 26905 * width: 320, 26906 * height: 240 26907 * }, { 26908 * custom_param: 1 26909 * }); 26910 */ 26911 self.windowManager = new WindowManager(self); 26912 26913 if (settings.encoding == 'xml') { 26914 self.on('GetContent', function(e) { 26915 if (e.save) { 26916 e.content = DOM.encode(e.content); 26917 } 26918 }); 26919 } 26920 26921 if (settings.add_form_submit_trigger) { 26922 self.on('submit', function() { 26923 if (self.initialized) { 26924 self.save(); 26925 } 26926 }); 26927 } 26928 26929 if (settings.add_unload_trigger) { 26930 self._beforeUnload = function() { 26931 if (self.initialized && !self.destroyed && !self.isHidden()) { 26932 self.save({format: 'raw', no_events: true, set_dirty: false}); 26933 } 26934 }; 26935 26936 self.editorManager.on('BeforeUnload', self._beforeUnload); 26937 } 26938 26939 // Load scripts 26940 function loadScripts() { 26941 var scriptLoader = ScriptLoader.ScriptLoader; 26942 26943 if (settings.language && settings.language != 'en' && !settings.language_url) { 26944 settings.language_url = self.editorManager.baseURL + '/langs/' + settings.language + '.js'; 26945 } 26946 26947 if (settings.language_url) { 26948 scriptLoader.add(settings.language_url); 26949 } 26950 26951 if (settings.theme && typeof settings.theme != "function" && 26952 settings.theme.charAt(0) != '-' && !ThemeManager.urls[settings.theme]) { 26953 var themeUrl = settings.theme_url; 26954 26955 if (themeUrl) { 26956 themeUrl = self.documentBaseURI.toAbsolute(themeUrl); 26957 } else { 26958 themeUrl = 'themes/' + settings.theme + '/theme' + suffix + '.js'; 26959 } 26960 26961 ThemeManager.load(settings.theme, themeUrl); 26962 } 26963 26964 if (Tools.isArray(settings.plugins)) { 26965 settings.plugins = settings.plugins.join(' '); 26966 } 26967 26968 each(settings.external_plugins, function(url, name) { 26969 PluginManager.load(name, url); 26970 settings.plugins += ' ' + name; 26971 }); 26972 26973 each(settings.plugins.split(/[ ,]/), function(plugin) { 26974 plugin = trim(plugin); 26975 26976 if (plugin && !PluginManager.urls[plugin]) { 26977 if (plugin.charAt(0) == '-') { 26978 plugin = plugin.substr(1, plugin.length); 26979 26980 var dependencies = PluginManager.dependencies(plugin); 26981 26982 each(dependencies, function(dep) { 26983 var defaultSettings = { 26984 prefix:'plugins/', 26985 resource: dep, 26986 suffix:'/plugin' + suffix + '.js' 26987 }; 26988 26989 dep = PluginManager.createUrl(defaultSettings, dep); 26990 PluginManager.load(dep.resource, dep); 26991 }); 26992 } else { 26993 PluginManager.load(plugin, { 26994 prefix: 'plugins/', 26995 resource: plugin, 26996 suffix: '/plugin' + suffix + '.js' 26997 }); 26998 } 26999 } 27000 }); 27001 27002 scriptLoader.loadQueue(function() { 27003 if (!self.removed) { 27004 self.init(); 27005 } 27006 }); 27007 } 27008 27009 loadScripts(); 27010 }, 27011 27012 /** 27013 * Initializes the editor this will be called automatically when 27014 * all plugins/themes and language packs are loaded by the rendered method. 27015 * This method will setup the iframe and create the theme and plugin instances. 27016 * 27017 * @method init 27018 */ 27019 init: function() { 27020 var self = this, settings = self.settings, elm = self.getElement(); 27021 var w, h, minHeight, n, o, Theme, url, bodyId, bodyClass, re, i, initializedPlugins = []; 27022 27023 self.rtl = this.editorManager.i18n.rtl; 27024 self.editorManager.add(self); 27025 27026 settings.aria_label = settings.aria_label || DOM.getAttrib(elm, 'aria-label', self.getLang('aria.rich_text_area')); 27027 27028 /** 27029 * Reference to the theme instance that was used to generate the UI. 27030 * 27031 * @property theme 27032 * @type tinymce.Theme 27033 * @example 27034 * // Executes a method on the theme directly 27035 * tinymce.activeEditor.theme.someMethod(); 27036 */ 27037 if (settings.theme) { 27038 if (typeof settings.theme != "function") { 27039 settings.theme = settings.theme.replace(/-/, ''); 27040 Theme = ThemeManager.get(settings.theme); 27041 self.theme = new Theme(self, ThemeManager.urls[settings.theme]); 27042 27043 if (self.theme.init) { 27044 self.theme.init(self, ThemeManager.urls[settings.theme] || self.documentBaseUrl.replace(/\/$/, '')); 27045 } 27046 } else { 27047 self.theme = settings.theme; 27048 } 27049 } 27050 27051 function initPlugin(plugin) { 27052 var Plugin = PluginManager.get(plugin), pluginUrl, pluginInstance; 27053 27054 pluginUrl = PluginManager.urls[plugin] || self.documentBaseUrl.replace(/\/$/, ''); 27055 plugin = trim(plugin); 27056 if (Plugin && inArray(initializedPlugins, plugin) === -1) { 27057 each(PluginManager.dependencies(plugin), function(dep){ 27058 initPlugin(dep); 27059 }); 27060 27061 pluginInstance = new Plugin(self, pluginUrl); 27062 27063 self.plugins[plugin] = pluginInstance; 27064 27065 if (pluginInstance.init) { 27066 pluginInstance.init(self, pluginUrl); 27067 initializedPlugins.push(plugin); 27068 } 27069 } 27070 } 27071 27072 // Create all plugins 27073 each(settings.plugins.replace(/\-/g, '').split(/[ ,]/), initPlugin); 27074 27075 // Measure box 27076 if (settings.render_ui && self.theme) { 27077 self.orgDisplay = elm.style.display; 27078 27079 if (typeof settings.theme != "function") { 27080 w = settings.width || elm.style.width || elm.offsetWidth; 27081 h = settings.height || elm.style.height || elm.offsetHeight; 27082 minHeight = settings.min_height || 100; 27083 re = /^[0-9\.]+(|px)$/i; 27084 27085 if (re.test('' + w)) { 27086 w = Math.max(parseInt(w, 10), 100); 27087 } 27088 27089 if (re.test('' + h)) { 27090 h = Math.max(parseInt(h, 10), minHeight); 27091 } 27092 27093 // Render UI 27094 o = self.theme.renderUI({ 27095 targetNode: elm, 27096 width: w, 27097 height: h, 27098 deltaWidth: settings.delta_width, 27099 deltaHeight: settings.delta_height 27100 }); 27101 27102 // Resize editor 27103 if (!settings.content_editable) { 27104 DOM.setStyles(o.sizeContainer || o.editorContainer, { 27105 wi2dth: w, 27106 // TODO: Fix this 27107 h2eight: h 27108 }); 27109 27110 h = (o.iframeHeight || h) + (typeof(h) == 'number' ? (o.deltaHeight || 0) : ''); 27111 if (h < minHeight) { 27112 h = minHeight; 27113 } 27114 } 27115 } else { 27116 o = settings.theme(self, elm); 27117 27118 // Convert element type to id:s 27119 if (o.editorContainer.nodeType) { 27120 o.editorContainer = o.editorContainer.id = o.editorContainer.id || self.id + "_parent"; 27121 } 27122 27123 // Convert element type to id:s 27124 if (o.iframeContainer.nodeType) { 27125 o.iframeContainer = o.iframeContainer.id = o.iframeContainer.id || self.id + "_iframecontainer"; 27126 } 27127 27128 // Use specified iframe height or the targets offsetHeight 27129 h = o.iframeHeight || elm.offsetHeight; 27130 } 27131 27132 self.editorContainer = o.editorContainer; 27133 } 27134 27135 // Load specified content CSS last 27136 if (settings.content_css) { 27137 each(explode(settings.content_css), function(u) { 27138 self.contentCSS.push(self.documentBaseURI.toAbsolute(u)); 27139 }); 27140 } 27141 27142 // Load specified content CSS last 27143 if (settings.content_style) { 27144 self.contentStyles.push(settings.content_style); 27145 } 27146 27147 // Content editable mode ends here 27148 if (settings.content_editable) { 27149 elm = n = o = null; // Fix IE leak 27150 return self.initContentBody(); 27151 } 27152 27153 self.iframeHTML = settings.doctype + '<html><head>'; 27154 27155 // We only need to override paths if we have to 27156 // IE has a bug where it remove site absolute urls to relative ones if this is specified 27157 if (settings.document_base_url != self.documentBaseUrl) { 27158 self.iframeHTML += '<base href="' + self.documentBaseURI.getURI() + '" />'; 27159 } 27160 27161 // IE8 doesn't support carets behind images setting ie7_compat would force IE8+ to run in IE7 compat mode. 27162 if (!Env.caretAfter && settings.ie7_compat) { 27163 self.iframeHTML += '<meta http-equiv="X-UA-Compatible" content="IE=7" />'; 27164 } 27165 27166 self.iframeHTML += '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />'; 27167 27168 // Load the CSS by injecting them into the HTML this will reduce "flicker" 27169 for (i = 0; i < self.contentCSS.length; i++) { 27170 var cssUrl = self.contentCSS[i]; 27171 self.iframeHTML += '<link type="text/css" rel="stylesheet" href="' + cssUrl + '" />'; 27172 self.loadedCSS[cssUrl] = true; 27173 } 27174 27175 bodyId = settings.body_id || 'tinymce'; 27176 if (bodyId.indexOf('=') != -1) { 27177 bodyId = self.getParam('body_id', '', 'hash'); 27178 bodyId = bodyId[self.id] || bodyId; 27179 } 27180 27181 bodyClass = settings.body_class || ''; 27182 if (bodyClass.indexOf('=') != -1) { 27183 bodyClass = self.getParam('body_class', '', 'hash'); 27184 bodyClass = bodyClass[self.id] || ''; 27185 } 27186 27187 self.iframeHTML += '</head><body id="' + bodyId + '" class="mce-content-body ' + bodyClass + '" ' + 27188 'onload="window.parent.tinymce.get(\'' + self.id + '\').fire(\'load\');"><br></body></html>'; 27189 27190 /*eslint no-script-url:0 */ 27191 var domainRelaxUrl = 'javascript:(function(){' + 27192 'document.open();document.domain="' + document.domain + '";' + 27193 'var ed = window.parent.tinymce.get("' + self.id + '");document.write(ed.iframeHTML);' + 27194 'document.close();ed.initContentBody(true);})()'; 27195 27196 // Domain relaxing is required since the user has messed around with document.domain 27197 if (document.domain != location.hostname) { 27198 url = domainRelaxUrl; 27199 } 27200 27201 // Create iframe 27202 // TODO: ACC add the appropriate description on this. 27203 n = DOM.add(o.iframeContainer, 'iframe', { 27204 id: self.id + "_ifr", 27205 src: url || 'javascript:""', // Workaround for HTTPS warning in IE6/7 27206 frameBorder: '0', 27207 allowTransparency: "true", 27208 title: self.editorManager.translate( 27209 "Rich Text Area. Press ALT-F9 for menu. " + 27210 "Press ALT-F10 for toolbar. Press ALT-0 for help" 27211 ), 27212 style: { 27213 width: '100%', 27214 height: h, 27215 display: 'block' // Important for Gecko to render the iframe correctly 27216 } 27217 }); 27218 27219 // Try accessing the document this will fail on IE when document.domain is set to the same as location.hostname 27220 // Then we have to force domain relaxing using the domainRelaxUrl approach very ugly!! 27221 if (ie) { 27222 try { 27223 self.getDoc(); 27224 } catch (e) { 27225 n.src = url = domainRelaxUrl; 27226 } 27227 } 27228 27229 self.contentAreaContainer = o.iframeContainer; 27230 27231 if (o.editorContainer) { 27232 DOM.get(o.editorContainer).style.display = self.orgDisplay; 27233 } 27234 27235 DOM.get(self.id).style.display = 'none'; 27236 DOM.setAttrib(self.id, 'aria-hidden', true); 27237 27238 if (!url) { 27239 self.initContentBody(); 27240 } 27241 27242 elm = n = o = null; // Cleanup 27243 }, 27244 27245 /** 27246 * This method get called by the init method ones the iframe is loaded. 27247 * It will fill the iframe with contents, setups DOM and selection objects for the iframe. 27248 * 27249 * @method initContentBody 27250 * @private 27251 */ 27252 initContentBody: function(skipWrite) { 27253 var self = this, settings = self.settings, targetElm = DOM.get(self.id), doc = self.getDoc(), body, contentCssText; 27254 27255 // Restore visibility on target element 27256 if (!settings.inline) { 27257 self.getElement().style.visibility = self.orgVisibility; 27258 } 27259 27260 // Setup iframe body 27261 if (!skipWrite && !settings.content_editable) { 27262 doc.open(); 27263 doc.write(self.iframeHTML); 27264 doc.close(); 27265 } 27266 27267 if (settings.content_editable) { 27268 self.on('remove', function() { 27269 var bodyEl = this.getBody(); 27270 27271 DOM.removeClass(bodyEl, 'mce-content-body'); 27272 DOM.removeClass(bodyEl, 'mce-edit-focus'); 27273 DOM.setAttrib(bodyEl, 'contentEditable', null); 27274 }); 27275 27276 DOM.addClass(targetElm, 'mce-content-body'); 27277 self.contentDocument = doc = settings.content_document || document; 27278 self.contentWindow = settings.content_window || window; 27279 self.bodyElement = targetElm; 27280 27281 // Prevent leak in IE 27282 settings.content_document = settings.content_window = null; 27283 27284 // TODO: Fix this 27285 settings.root_name = targetElm.nodeName.toLowerCase(); 27286 } 27287 27288 // It will not steal focus while setting contentEditable 27289 body = self.getBody(); 27290 body.disabled = true; 27291 27292 if (!settings.readonly) { 27293 if (self.inline && DOM.getStyle(body, 'position', true) == 'static') { 27294 body.style.position = 'relative'; 27295 } 27296 27297 body.contentEditable = self.getParam('content_editable_state', true); 27298 } 27299 27300 body.disabled = false; 27301 27302 /** 27303 * Schema instance, enables you to validate elements and it's children. 27304 * 27305 * @property schema 27306 * @type tinymce.html.Schema 27307 */ 27308 self.schema = new Schema(settings); 27309 27310 /** 27311 * DOM instance for the editor. 27312 * 27313 * @property dom 27314 * @type tinymce.dom.DOMUtils 27315 * @example 27316 * // Adds a class to all paragraphs within the editor 27317 * tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('p'), 'someclass'); 27318 */ 27319 self.dom = new DOMUtils(doc, { 27320 keep_values: true, 27321 url_converter: self.convertURL, 27322 url_converter_scope: self, 27323 hex_colors: settings.force_hex_style_colors, 27324 class_filter: settings.class_filter, 27325 update_styles: true, 27326 root_element: settings.content_editable ? self.id : null, 27327 collect: settings.content_editable, 27328 schema: self.schema, 27329 onSetAttrib: function(e) { 27330 self.fire('SetAttrib', e); 27331 } 27332 }); 27333 27334 /** 27335 * HTML parser will be used when contents is inserted into the editor. 27336 * 27337 * @property parser 27338 * @type tinymce.html.DomParser 27339 */ 27340 self.parser = new DomParser(settings, self.schema); 27341 27342 // Convert src and href into data-mce-src, data-mce-href and data-mce-style 27343 self.parser.addAttributeFilter('src,href,style,tabindex', function(nodes, name) { 27344 var i = nodes.length, node, dom = self.dom, value, internalName; 27345 27346 while (i--) { 27347 node = nodes[i]; 27348 value = node.attr(name); 27349 internalName = 'data-mce-' + name; 27350 27351 // Add internal attribute if we need to we don't on a refresh of the document 27352 if (!node.attributes.map[internalName]) { 27353 if (name === "style") { 27354 node.attr(internalName, dom.serializeStyle(dom.parseStyle(value), node.name)); 27355 } else if (name === "tabindex") { 27356 node.attr(internalName, value); 27357 node.attr(name, null); 27358 } else { 27359 node.attr(internalName, self.convertURL(value, name, node.name)); 27360 } 27361 } 27362 } 27363 }); 27364 27365 // Keep scripts from executing 27366 self.parser.addNodeFilter('script', function(nodes) { 27367 var i = nodes.length, node; 27368 27369 while (i--) { 27370 node = nodes[i]; 27371 node.attr('type', 'mce-' + (node.attr('type') || 'text/javascript')); 27372 } 27373 }); 27374 27375 self.parser.addNodeFilter('#cdata', function(nodes) { 27376 var i = nodes.length, node; 27377 27378 while (i--) { 27379 node = nodes[i]; 27380 node.type = 8; 27381 node.name = '#comment'; 27382 node.value = '[CDATA[' + node.value + ']]'; 27383 } 27384 }); 27385 27386 self.parser.addNodeFilter('p,h1,h2,h3,h4,h5,h6,div', function(nodes) { 27387 var i = nodes.length, node, nonEmptyElements = self.schema.getNonEmptyElements(); 27388 27389 while (i--) { 27390 node = nodes[i]; 27391 27392 if (node.isEmpty(nonEmptyElements)) { 27393 node.empty().append(new Node('br', 1)).shortEnded = true; 27394 } 27395 } 27396 }); 27397 27398 /** 27399 * DOM serializer for the editor. Will be used when contents is extracted from the editor. 27400 * 27401 * @property serializer 27402 * @type tinymce.dom.Serializer 27403 * @example 27404 * // Serializes the first paragraph in the editor into a string 27405 * tinymce.activeEditor.serializer.serialize(tinymce.activeEditor.dom.select('p')[0]); 27406 */ 27407 self.serializer = new DomSerializer(settings, self); 27408 27409 /** 27410 * Selection instance for the editor. 27411 * 27412 * @property selection 27413 * @type tinymce.dom.Selection 27414 * @example 27415 * // Sets some contents to the current selection in the editor 27416 * tinymce.activeEditor.selection.setContent('Some contents'); 27417 * 27418 * // Gets the current selection 27419 * alert(tinymce.activeEditor.selection.getContent()); 27420 * 27421 * // Selects the first paragraph found 27422 * tinymce.activeEditor.selection.select(tinymce.activeEditor.dom.select('p')[0]); 27423 */ 27424 self.selection = new Selection(self.dom, self.getWin(), self.serializer, self); 27425 27426 /** 27427 * Formatter instance. 27428 * 27429 * @property formatter 27430 * @type tinymce.Formatter 27431 */ 27432 self.formatter = new Formatter(self); 27433 27434 /** 27435 * Undo manager instance, responsible for handling undo levels. 27436 * 27437 * @property undoManager 27438 * @type tinymce.UndoManager 27439 * @example 27440 * // Undoes the last modification to the editor 27441 * tinymce.activeEditor.undoManager.undo(); 27442 */ 27443 self.undoManager = new UndoManager(self); 27444 27445 self.forceBlocks = new ForceBlocks(self); 27446 self.enterKey = new EnterKey(self); 27447 self.editorCommands = new EditorCommands(self); 27448 27449 self.fire('PreInit'); 27450 27451 if (!settings.browser_spellcheck && !settings.gecko_spellcheck) { 27452 doc.body.spellcheck = false; // Gecko 27453 DOM.setAttrib(body, "spellcheck", "false"); 27454 } 27455 27456 self.fire('PostRender'); 27457 27458 self.quirks = Quirks(self); 27459 27460 if (settings.directionality) { 27461 body.dir = settings.directionality; 27462 } 27463 27464 if (settings.nowrap) { 27465 body.style.whiteSpace = "nowrap"; 27466 } 27467 27468 if (settings.protect) { 27469 self.on('BeforeSetContent', function(e) { 27470 each(settings.protect, function(pattern) { 27471 e.content = e.content.replace(pattern, function(str) { 27472 return '<!--mce:protected ' + escape(str) + '-->'; 27473 }); 27474 }); 27475 }); 27476 } 27477 27478 self.on('SetContent', function() { 27479 self.addVisual(self.getBody()); 27480 }); 27481 27482 // Remove empty contents 27483 if (settings.padd_empty_editor) { 27484 self.on('PostProcess', function(e) { 27485 e.content = e.content.replace(/^(<p[^>]*>( | |\s|\u00a0|)<\/p>[\r\n]*|<br \/>[\r\n]*)$/, ''); 27486 }); 27487 } 27488 27489 self.load({initial: true, format: 'html'}); 27490 self.startContent = self.getContent({format: 'raw'}); 27491 27492 /** 27493 * Is set to true after the editor instance has been initialized 27494 * 27495 * @property initialized 27496 * @type Boolean 27497 * @example 27498 * function isEditorInitialized(editor) { 27499 * return editor && editor.initialized; 27500 * } 27501 */ 27502 self.initialized = true; 27503 self.bindPendingEventDelegates(); 27504 27505 self.fire('init'); 27506 self.focus(true); 27507 self.nodeChanged({initial: true}); 27508 self.execCallback('init_instance_callback', self); 27509 27510 // Add editor specific CSS styles 27511 if (self.contentStyles.length > 0) { 27512 contentCssText = ''; 27513 27514 each(self.contentStyles, function(style) { 27515 contentCssText += style + "\r\n"; 27516 }); 27517 27518 self.dom.addStyle(contentCssText); 27519 } 27520 27521 // Load specified content CSS last 27522 each(self.contentCSS, function(cssUrl) { 27523 if (!self.loadedCSS[cssUrl]) { 27524 self.dom.loadCSS(cssUrl); 27525 self.loadedCSS[cssUrl] = true; 27526 } 27527 }); 27528 27529 // Handle auto focus 27530 if (settings.auto_focus) { 27531 setTimeout(function () { 27532 var ed = self.editorManager.get(settings.auto_focus); 27533 27534 ed.selection.select(ed.getBody(), 1); 27535 ed.selection.collapse(1); 27536 ed.getBody().focus(); 27537 ed.getWin().focus(); 27538 }, 100); 27539 } 27540 27541 // Clean up references for IE 27542 targetElm = doc = body = null; 27543 }, 27544 27545 /** 27546 * Focuses/activates the editor. This will set this editor as the activeEditor in the tinymce collection 27547 * it will also place DOM focus inside the editor. 27548 * 27549 * @method focus 27550 * @param {Boolean} skip_focus Skip DOM focus. Just set is as the active editor. 27551 */ 27552 focus: function(skip_focus) { 27553 var oed, self = this, selection = self.selection, contentEditable = self.settings.content_editable, rng; 27554 var controlElm, doc = self.getDoc(), body; 27555 27556 if (!skip_focus) { 27557 // Get selected control element 27558 rng = selection.getRng(); 27559 if (rng.item) { 27560 controlElm = rng.item(0); 27561 } 27562 27563 self._refreshContentEditable(); 27564 27565 // Focus the window iframe 27566 if (!contentEditable) { 27567 // WebKit needs this call to fire focusin event properly see #5948 27568 // But Opera pre Blink engine will produce an empty selection so skip Opera 27569 if (!Env.opera) { 27570 self.getBody().focus(); 27571 } 27572 27573 self.getWin().focus(); 27574 } 27575 27576 // Focus the body as well since it's contentEditable 27577 if (isGecko || contentEditable) { 27578 body = self.getBody(); 27579 27580 // Check for setActive since it doesn't scroll to the element 27581 if (body.setActive) { 27582 // IE 11 sometimes throws "Invalid function" then fallback to focus 27583 try { 27584 body.setActive(); 27585 } catch (ex) { 27586 body.focus(); 27587 } 27588 } else { 27589 body.focus(); 27590 } 27591 27592 if (contentEditable) { 27593 selection.normalize(); 27594 } 27595 } 27596 27597 // Restore selected control element 27598 // This is needed when for example an image is selected within a 27599 // layer a call to focus will then remove the control selection 27600 if (controlElm && controlElm.ownerDocument == doc) { 27601 rng = doc.body.createControlRange(); 27602 rng.addElement(controlElm); 27603 rng.select(); 27604 } 27605 } 27606 27607 if (self.editorManager.activeEditor != self) { 27608 if ((oed = self.editorManager.activeEditor)) { 27609 oed.fire('deactivate', {relatedTarget: self}); 27610 } 27611 27612 self.fire('activate', {relatedTarget: oed}); 27613 } 27614 27615 self.editorManager.activeEditor = self; 27616 }, 27617 27618 /** 27619 * Executes a legacy callback. This method is useful to call old 2.x option callbacks. 27620 * There new event model is a better way to add callback so this method might be removed in the future. 27621 * 27622 * @method execCallback 27623 * @param {String} name Name of the callback to execute. 27624 * @return {Object} Return value passed from callback function. 27625 */ 27626 execCallback: function(name) { 27627 var self = this, callback = self.settings[name], scope; 27628 27629 if (!callback) { 27630 return; 27631 } 27632 27633 // Look through lookup 27634 if (self.callbackLookup && (scope = self.callbackLookup[name])) { 27635 callback = scope.func; 27636 scope = scope.scope; 27637 } 27638 27639 if (typeof(callback) === 'string') { 27640 scope = callback.replace(/\.\w+$/, ''); 27641 scope = scope ? resolve(scope) : 0; 27642 callback = resolve(callback); 27643 self.callbackLookup = self.callbackLookup || {}; 27644 self.callbackLookup[name] = {func: callback, scope: scope}; 27645 } 27646 27647 return callback.apply(scope || self, Array.prototype.slice.call(arguments, 1)); 27648 }, 27649 27650 /** 27651 * Translates the specified string by replacing variables with language pack items it will also check if there is 27652 * a key mathcin the input. 27653 * 27654 * @method translate 27655 * @param {String} text String to translate by the language pack data. 27656 * @return {String} Translated string. 27657 */ 27658 translate: function(text) { 27659 var lang = this.settings.language || 'en', i18n = this.editorManager.i18n; 27660 27661 if (!text) { 27662 return ''; 27663 } 27664 27665 return i18n.data[lang + '.' + text] || text.replace(/\{\#([^\}]+)\}/g, function(a, b) { 27666 return i18n.data[lang + '.' + b] || '{#' + b + '}'; 27667 }); 27668 }, 27669 27670 /** 27671 * Returns a language pack item by name/key. 27672 * 27673 * @method getLang 27674 * @param {String} name Name/key to get from the language pack. 27675 * @param {String} defaultVal Optional default value to retrive. 27676 */ 27677 getLang: function(name, defaultVal) { 27678 return ( 27679 this.editorManager.i18n.data[(this.settings.language || 'en') + '.' + name] || 27680 (defaultVal !== undefined ? defaultVal : '{#' + name + '}') 27681 ); 27682 }, 27683 27684 /** 27685 * Returns a configuration parameter by name. 27686 * 27687 * @method getParam 27688 * @param {String} name Configruation parameter to retrive. 27689 * @param {String} defaultVal Optional default value to return. 27690 * @param {String} type Optional type parameter. 27691 * @return {String} Configuration parameter value or default value. 27692 * @example 27693 * // Returns a specific config value from the currently active editor 27694 * var someval = tinymce.activeEditor.getParam('myvalue'); 27695 * 27696 * // Returns a specific config value from a specific editor instance by id 27697 * var someval2 = tinymce.get('my_editor').getParam('myvalue'); 27698 */ 27699 getParam: function(name, defaultVal, type) { 27700 var value = name in this.settings ? this.settings[name] : defaultVal, output; 27701 27702 if (type === 'hash') { 27703 output = {}; 27704 27705 if (typeof(value) === 'string') { 27706 each(value.indexOf('=') > 0 ? value.split(/[;,](?![^=;,]*(?:[;,]|$))/) : value.split(','), function(value) { 27707 value = value.split('='); 27708 27709 if (value.length > 1) { 27710 output[trim(value[0])] = trim(value[1]); 27711 } else { 27712 output[trim(value[0])] = trim(value); 27713 } 27714 }); 27715 } else { 27716 output = value; 27717 } 27718 27719 return output; 27720 } 27721 27722 return value; 27723 }, 27724 27725 /** 27726 * Distpaches out a onNodeChange event to all observers. This method should be called when you 27727 * need to update the UI states or element path etc. 27728 * 27729 * @method nodeChanged 27730 */ 27731 nodeChanged: function() { 27732 var self = this, selection = self.selection, node, parents, root; 27733 27734 // Fix for bug #1896577 it seems that this can not be fired while the editor is loading 27735 if (self.initialized && !self.settings.disable_nodechange && !self.settings.readonly) { 27736 // Get start node 27737 root = self.getBody(); 27738 node = selection.getStart() || root; 27739 node = ie && node.ownerDocument != self.getDoc() ? self.getBody() : node; // Fix for IE initial state 27740 27741 // Edge case for <p>|<img></p> 27742 if (node.nodeName == 'IMG' && selection.isCollapsed()) { 27743 node = node.parentNode; 27744 } 27745 27746 // Get parents and add them to object 27747 parents = []; 27748 self.dom.getParent(node, function(node) { 27749 if (node === root) { 27750 return true; 27751 } 27752 27753 parents.push(node); 27754 }); 27755 27756 self.fire('NodeChange', {element: node, parents: parents}); 27757 } 27758 }, 27759 27760 /** 27761 * Adds a button that later gets created by the theme in the editors toolbars. 27762 * 27763 * @method addButton 27764 * @param {String} name Button name to add. 27765 * @param {Object} settings Settings object with title, cmd etc. 27766 * @example 27767 * // Adds a custom button to the editor that inserts contents when clicked 27768 * tinymce.init({ 27769 * ... 27770 * 27771 * toolbar: 'example' 27772 * 27773 * setup: function(ed) { 27774 * ed.addButton('example', { 27775 * title: 'My title', 27776 * image: '../js/tinymce/plugins/example/img/example.gif', 27777 * onclick: function() { 27778 * ed.insertContent('Hello world!!'); 27779 * } 27780 * }); 27781 * } 27782 * }); 27783 */ 27784 addButton: function(name, settings) { 27785 var self = this; 27786 27787 if (settings.cmd) { 27788 settings.onclick = function() { 27789 self.execCommand(settings.cmd); 27790 }; 27791 } 27792 27793 if (!settings.text && !settings.icon) { 27794 settings.icon = name; 27795 } 27796 27797 self.buttons = self.buttons || {}; 27798 settings.tooltip = settings.tooltip || settings.title; 27799 self.buttons[name] = settings; 27800 }, 27801 27802 /** 27803 * Adds a menu item to be used in the menus of the theme. There might be multiple instances 27804 * of this menu item for example it might be used in the main menus of the theme but also in 27805 * the context menu so make sure that it's self contained and supports multiple instances. 27806 * 27807 * @method addMenuItem 27808 * @param {String} name Menu item name to add. 27809 * @param {Object} settings Settings object with title, cmd etc. 27810 * @example 27811 * // Adds a custom menu item to the editor that inserts contents when clicked 27812 * // The context option allows you to add the menu item to an existing default menu 27813 * tinymce.init({ 27814 * ... 27815 * 27816 * setup: function(ed) { 27817 * ed.addMenuItem('example', { 27818 * text: 'My menu item', 27819 * context: 'tools', 27820 * onclick: function() { 27821 * ed.insertContent('Hello world!!'); 27822 * } 27823 * }); 27824 * } 27825 * }); 27826 */ 27827 addMenuItem: function(name, settings) { 27828 var self = this; 27829 27830 if (settings.cmd) { 27831 settings.onclick = function() { 27832 self.execCommand(settings.cmd); 27833 }; 27834 } 27835 27836 self.menuItems = self.menuItems || {}; 27837 self.menuItems[name] = settings; 27838 }, 27839 27840 /** 27841 * Adds a custom command to the editor, you can also override existing commands with this method. 27842 * The command that you add can be executed with execCommand. 27843 * 27844 * @method addCommand 27845 * @param {String} name Command name to add/override. 27846 * @param {addCommandCallback} callback Function to execute when the command occurs. 27847 * @param {Object} scope Optional scope to execute the function in. 27848 * @example 27849 * // Adds a custom command that later can be executed using execCommand 27850 * tinymce.init({ 27851 * ... 27852 * 27853 * setup: function(ed) { 27854 * // Register example command 27855 * ed.addCommand('mycommand', function(ui, v) { 27856 * ed.windowManager.alert('Hello world!! Selection: ' + ed.selection.getContent({format: 'text'})); 27857 * }); 27858 * } 27859 * }); 27860 */ 27861 addCommand: function(name, callback, scope) { 27862 /** 27863 * Callback function that gets called when a command is executed. 27864 * 27865 * @callback addCommandCallback 27866 * @param {Boolean} ui Display UI state true/false. 27867 * @param {Object} value Optional value for command. 27868 * @return {Boolean} True/false state if the command was handled or not. 27869 */ 27870 this.execCommands[name] = {func: callback, scope: scope || this}; 27871 }, 27872 27873 /** 27874 * Adds a custom query state command to the editor, you can also override existing commands with this method. 27875 * The command that you add can be executed with queryCommandState function. 27876 * 27877 * @method addQueryStateHandler 27878 * @param {String} name Command name to add/override. 27879 * @param {addQueryStateHandlerCallback} callback Function to execute when the command state retrival occurs. 27880 * @param {Object} scope Optional scope to execute the function in. 27881 */ 27882 addQueryStateHandler: function(name, callback, scope) { 27883 /** 27884 * Callback function that gets called when a queryCommandState is executed. 27885 * 27886 * @callback addQueryStateHandlerCallback 27887 * @return {Boolean} True/false state if the command is enabled or not like is it bold. 27888 */ 27889 this.queryStateCommands[name] = {func: callback, scope: scope || this}; 27890 }, 27891 27892 /** 27893 * Adds a custom query value command to the editor, you can also override existing commands with this method. 27894 * The command that you add can be executed with queryCommandValue function. 27895 * 27896 * @method addQueryValueHandler 27897 * @param {String} name Command name to add/override. 27898 * @param {addQueryValueHandlerCallback} callback Function to execute when the command value retrival occurs. 27899 * @param {Object} scope Optional scope to execute the function in. 27900 */ 27901 addQueryValueHandler: function(name, callback, scope) { 27902 /** 27903 * Callback function that gets called when a queryCommandValue is executed. 27904 * 27905 * @callback addQueryValueHandlerCallback 27906 * @return {Object} Value of the command or undefined. 27907 */ 27908 this.queryValueCommands[name] = {func: callback, scope: scope || this}; 27909 }, 27910 27911 /** 27912 * Adds a keyboard shortcut for some command or function. 27913 * 27914 * @method addShortcut 27915 * @param {String} pattern Shortcut pattern. Like for example: ctrl+alt+o. 27916 * @param {String} desc Text description for the command. 27917 * @param {String/Function} cmdFunc Command name string or function to execute when the key is pressed. 27918 * @param {Object} sc Optional scope to execute the function in. 27919 * @return {Boolean} true/false state if the shortcut was added or not. 27920 */ 27921 addShortcut: function(pattern, desc, cmdFunc, scope) { 27922 this.shortcuts.add(pattern, desc, cmdFunc, scope); 27923 }, 27924 27925 /** 27926 * Executes a command on the current instance. These commands can be TinyMCE internal commands prefixed with "mce" or 27927 * they can be build in browser commands such as "Bold". A compleate list of browser commands is available on MSDN or Mozilla.org. 27928 * This function will dispatch the execCommand function on each plugin, theme or the execcommand_callback option if none of these 27929 * return true it will handle the command as a internal browser command. 27930 * 27931 * @method execCommand 27932 * @param {String} cmd Command name to execute, for example mceLink or Bold. 27933 * @param {Boolean} ui True/false state if a UI (dialog) should be presented or not. 27934 * @param {mixed} value Optional command value, this can be anything. 27935 * @param {Object} a Optional arguments object. 27936 */ 27937 execCommand: function(cmd, ui, value, args) { 27938 var self = this, state = 0, cmdItem; 27939 27940 if (!/^(mceAddUndoLevel|mceEndUndoLevel|mceBeginUndoLevel|mceRepaint)$/.test(cmd) && (!args || !args.skip_focus)) { 27941 self.focus(); 27942 } 27943 27944 args = extend({}, args); 27945 args = self.fire('BeforeExecCommand', {command: cmd, ui: ui, value: value}); 27946 if (args.isDefaultPrevented()) { 27947 return false; 27948 } 27949 27950 // Registred commands 27951 if ((cmdItem = self.execCommands[cmd])) { 27952 // Fall through on true 27953 if (cmdItem.func.call(cmdItem.scope, ui, value) !== true) { 27954 self.fire('ExecCommand', {command: cmd, ui: ui, value: value}); 27955 return true; 27956 } 27957 } 27958 27959 // Plugin commands 27960 each(self.plugins, function(p) { 27961 if (p.execCommand && p.execCommand(cmd, ui, value)) { 27962 self.fire('ExecCommand', {command: cmd, ui: ui, value: value}); 27963 state = true; 27964 return false; 27965 } 27966 }); 27967 27968 if (state) { 27969 return state; 27970 } 27971 27972 // Theme commands 27973 if (self.theme && self.theme.execCommand && self.theme.execCommand(cmd, ui, value)) { 27974 self.fire('ExecCommand', {command: cmd, ui: ui, value: value}); 27975 return true; 27976 } 27977 27978 // Editor commands 27979 if (self.editorCommands.execCommand(cmd, ui, value)) { 27980 self.fire('ExecCommand', {command: cmd, ui: ui, value: value}); 27981 return true; 27982 } 27983 27984 // Browser commands 27985 self.getDoc().execCommand(cmd, ui, value); 27986 self.fire('ExecCommand', {command: cmd, ui: ui, value: value}); 27987 }, 27988 27989 /** 27990 * Returns a command specific state, for example if bold is enabled or not. 27991 * 27992 * @method queryCommandState 27993 * @param {string} cmd Command to query state from. 27994 * @return {Boolean} Command specific state, for example if bold is enabled or not. 27995 */ 27996 queryCommandState: function(cmd) { 27997 var self = this, queryItem, returnVal; 27998 27999 // Is hidden then return undefined 28000 if (self._isHidden()) { 28001 return; 28002 } 28003 28004 // Registred commands 28005 if ((queryItem = self.queryStateCommands[cmd])) { 28006 returnVal = queryItem.func.call(queryItem.scope); 28007 28008 // Fall though on true 28009 if (returnVal !== true) { 28010 return returnVal; 28011 } 28012 } 28013 28014 // Editor commands 28015 returnVal = self.editorCommands.queryCommandState(cmd); 28016 if (returnVal !== -1) { 28017 return returnVal; 28018 } 28019 28020 // Browser commands 28021 try { 28022 return self.getDoc().queryCommandState(cmd); 28023 } catch (ex) { 28024 // Fails sometimes see bug: 1896577 28025 } 28026 }, 28027 28028 /** 28029 * Returns a command specific value, for example the current font size. 28030 * 28031 * @method queryCommandValue 28032 * @param {string} cmd Command to query value from. 28033 * @return {Object} Command specific value, for example the current font size. 28034 */ 28035 queryCommandValue: function(cmd) { 28036 var self = this, queryItem, returnVal; 28037 28038 // Is hidden then return undefined 28039 if (self._isHidden()) { 28040 return; 28041 } 28042 28043 // Registred commands 28044 if ((queryItem = self.queryValueCommands[cmd])) { 28045 returnVal = queryItem.func.call(queryItem.scope); 28046 28047 // Fall though on true 28048 if (returnVal !== true) { 28049 return returnVal; 28050 } 28051 } 28052 28053 // Editor commands 28054 returnVal = self.editorCommands.queryCommandValue(cmd); 28055 if (returnVal !== undefined) { 28056 return returnVal; 28057 } 28058 28059 // Browser commands 28060 try { 28061 return self.getDoc().queryCommandValue(cmd); 28062 } catch (ex) { 28063 // Fails sometimes see bug: 1896577 28064 } 28065 }, 28066 28067 /** 28068 * Shows the editor and hides any textarea/div that the editor is supposed to replace. 28069 * 28070 * @method show 28071 */ 28072 show: function() { 28073 var self = this; 28074 28075 if (self.hidden) { 28076 self.hidden = false; 28077 28078 if (self.inline) { 28079 self.getBody().contentEditable = true; 28080 } else { 28081 DOM.show(self.getContainer()); 28082 DOM.hide(self.id); 28083 } 28084 28085 self.load(); 28086 self.fire('show'); 28087 } 28088 }, 28089 28090 /** 28091 * Hides the editor and shows any textarea/div that the editor is supposed to replace. 28092 * 28093 * @method hide 28094 */ 28095 hide: function() { 28096 var self = this, doc = self.getDoc(); 28097 28098 if (!self.hidden) { 28099 self.hidden = true; 28100 28101 // Fixed bug where IE has a blinking cursor left from the editor 28102 if (ie && doc && !self.inline) { 28103 doc.execCommand('SelectAll'); 28104 } 28105 28106 // We must save before we hide so Safari doesn't crash 28107 self.save(); 28108 28109 if (self.inline) { 28110 self.getBody().contentEditable = false; 28111 28112 // Make sure the editor gets blurred 28113 if (self == self.editorManager.focusedEditor) { 28114 self.editorManager.focusedEditor = null; 28115 } 28116 } else { 28117 DOM.hide(self.getContainer()); 28118 DOM.setStyle(self.id, 'display', self.orgDisplay); 28119 } 28120 28121 self.fire('hide'); 28122 } 28123 }, 28124 28125 /** 28126 * Returns true/false if the editor is hidden or not. 28127 * 28128 * @method isHidden 28129 * @return {Boolean} True/false if the editor is hidden or not. 28130 */ 28131 isHidden: function() { 28132 return !!this.hidden; 28133 }, 28134 28135 /** 28136 * Sets the progress state, this will display a throbber/progess for the editor. 28137 * This is ideal for asycronous operations like an AJAX save call. 28138 * 28139 * @method setProgressState 28140 * @param {Boolean} state Boolean state if the progress should be shown or hidden. 28141 * @param {Number} time Optional time to wait before the progress gets shown. 28142 * @return {Boolean} Same as the input state. 28143 * @example 28144 * // Show progress for the active editor 28145 * tinymce.activeEditor.setProgressState(true); 28146 * 28147 * // Hide progress for the active editor 28148 * tinymce.activeEditor.setProgressState(false); 28149 * 28150 * // Show progress after 3 seconds 28151 * tinymce.activeEditor.setProgressState(true, 3000); 28152 */ 28153 setProgressState: function(state, time) { 28154 this.fire('ProgressState', {state: state, time: time}); 28155 }, 28156 28157 /** 28158 * Loads contents from the textarea or div element that got converted into an editor instance. 28159 * This method will move the contents from that textarea or div into the editor by using setContent 28160 * so all events etc that method has will get dispatched as well. 28161 * 28162 * @method load 28163 * @param {Object} args Optional content object, this gets passed around through the whole load process. 28164 * @return {String} HTML string that got set into the editor. 28165 */ 28166 load: function(args) { 28167 var self = this, elm = self.getElement(), html; 28168 28169 if (elm) { 28170 args = args || {}; 28171 args.load = true; 28172 28173 html = self.setContent(elm.value !== undefined ? elm.value : elm.innerHTML, args); 28174 args.element = elm; 28175 28176 if (!args.no_events) { 28177 self.fire('LoadContent', args); 28178 } 28179 28180 args.element = elm = null; 28181 28182 return html; 28183 } 28184 }, 28185 28186 /** 28187 * Saves the contents from a editor out to the textarea or div element that got converted into an editor instance. 28188 * This method will move the HTML contents from the editor into that textarea or div by getContent 28189 * so all events etc that method has will get dispatched as well. 28190 * 28191 * @method save 28192 * @param {Object} args Optional content object, this gets passed around through the whole save process. 28193 * @return {String} HTML string that got set into the textarea/div. 28194 */ 28195 save: function(args) { 28196 var self = this, elm = self.getElement(), html, form; 28197 28198 if (!elm || !self.initialized) { 28199 return; 28200 } 28201 28202 args = args || {}; 28203 args.save = true; 28204 28205 args.element = elm; 28206 html = args.content = self.getContent(args); 28207 28208 if (!args.no_events) { 28209 self.fire('SaveContent', args); 28210 } 28211 28212 html = args.content; 28213 28214 if (!/TEXTAREA|INPUT/i.test(elm.nodeName)) { 28215 // Update DIV element when not in inline mode 28216 if (!self.inline) { 28217 elm.innerHTML = html; 28218 } 28219 28220 // Update hidden form element 28221 if ((form = DOM.getParent(self.id, 'form'))) { 28222 each(form.elements, function(elm) { 28223 if (elm.name == self.id) { 28224 elm.value = html; 28225 return false; 28226 } 28227 }); 28228 } 28229 } else { 28230 elm.value = html; 28231 } 28232 28233 args.element = elm = null; 28234 28235 if (args.set_dirty !== false) { 28236 self.isNotDirty = true; 28237 } 28238 28239 return html; 28240 }, 28241 28242 /** 28243 * Sets the specified content to the editor instance, this will cleanup the content before it gets set using 28244 * the different cleanup rules options. 28245 * 28246 * @method setContent 28247 * @param {String} content Content to set to editor, normally HTML contents but can be other formats as well. 28248 * @param {Object} args Optional content object, this gets passed around through the whole set process. 28249 * @return {String} HTML string that got set into the editor. 28250 * @example 28251 * // Sets the HTML contents of the activeEditor editor 28252 * tinymce.activeEditor.setContent('<span>some</span> html'); 28253 * 28254 * // Sets the raw contents of the activeEditor editor 28255 * tinymce.activeEditor.setContent('<span>some</span> html', {format: 'raw'}); 28256 * 28257 * // Sets the content of a specific editor (my_editor in this example) 28258 * tinymce.get('my_editor').setContent(data); 28259 * 28260 * // Sets the bbcode contents of the activeEditor editor if the bbcode plugin was added 28261 * tinymce.activeEditor.setContent('[b]some[/b] html', {format: 'bbcode'}); 28262 */ 28263 setContent: function(content, args) { 28264 var self = this, body = self.getBody(), forcedRootBlockName; 28265 28266 // Setup args object 28267 args = args || {}; 28268 args.format = args.format || 'html'; 28269 args.set = true; 28270 args.content = content; 28271 28272 // Do preprocessing 28273 if (!args.no_events) { 28274 self.fire('BeforeSetContent', args); 28275 } 28276 28277 content = args.content; 28278 28279 // Padd empty content in Gecko and Safari. Commands will otherwise fail on the content 28280 // It will also be impossible to place the caret in the editor unless there is a BR element present 28281 if (content.length === 0 || /^\s+$/.test(content)) { 28282 forcedRootBlockName = self.settings.forced_root_block; 28283 28284 // Check if forcedRootBlock is configured and that the block is a valid child of the body 28285 if (forcedRootBlockName && self.schema.isValidChild(body.nodeName.toLowerCase(), forcedRootBlockName.toLowerCase())) { 28286 // Padd with bogus BR elements on modern browsers and IE 7 and 8 since they don't render empty P tags properly 28287 content = ie && ie < 11 ? '' : '<br data-mce-bogus="1">'; 28288 content = self.dom.createHTML(forcedRootBlockName, self.settings.forced_root_block_attrs, content); 28289 } else if (!ie) { 28290 // We need to add a BR when forced_root_block is disabled on non IE browsers to place the caret 28291 content = '<br data-mce-bogus="1">'; 28292 } 28293 28294 body.innerHTML = content; 28295 28296 self.fire('SetContent', args); 28297 } else { 28298 // Parse and serialize the html 28299 if (args.format !== 'raw') { 28300 content = new Serializer({}, self.schema).serialize( 28301 self.parser.parse(content, {isRootContent: true}) 28302 ); 28303 } 28304 28305 // Set the new cleaned contents to the editor 28306 args.content = trim(content); 28307 self.dom.setHTML(body, args.content); 28308 28309 // Do post processing 28310 if (!args.no_events) { 28311 self.fire('SetContent', args); 28312 } 28313 28314 // Don't normalize selection if the focused element isn't the body in 28315 // content editable mode since it will steal focus otherwise 28316 /*if (!self.settings.content_editable || document.activeElement === self.getBody()) { 28317 self.selection.normalize(); 28318 }*/ 28319 } 28320 28321 return args.content; 28322 }, 28323 28324 /** 28325 * Gets the content from the editor instance, this will cleanup the content before it gets returned using 28326 * the different cleanup rules options. 28327 * 28328 * @method getContent 28329 * @param {Object} args Optional content object, this gets passed around through the whole get process. 28330 * @return {String} Cleaned content string, normally HTML contents. 28331 * @example 28332 * // Get the HTML contents of the currently active editor 28333 * console.debug(tinymce.activeEditor.getContent()); 28334 * 28335 * // Get the raw contents of the currently active editor 28336 * tinymce.activeEditor.getContent({format: 'raw'}); 28337 * 28338 * // Get content of a specific editor: 28339 * tinymce.get('content id').getContent() 28340 */ 28341 getContent: function(args) { 28342 var self = this, content, body = self.getBody(); 28343 28344 // Setup args object 28345 args = args || {}; 28346 args.format = args.format || 'html'; 28347 args.get = true; 28348 args.getInner = true; 28349 28350 // Do preprocessing 28351 if (!args.no_events) { 28352 self.fire('BeforeGetContent', args); 28353 } 28354 28355 // Get raw contents or by default the cleaned contents 28356 if (args.format == 'raw') { 28357 content = body.innerHTML; 28358 } else if (args.format == 'text') { 28359 content = body.innerText || body.textContent; 28360 } else { 28361 content = self.serializer.serialize(body, args); 28362 } 28363 28364 // Trim whitespace in beginning/end of HTML 28365 if (args.format != 'text') { 28366 args.content = trim(content); 28367 } else { 28368 args.content = content; 28369 } 28370 28371 // Do post processing 28372 if (!args.no_events) { 28373 self.fire('GetContent', args); 28374 } 28375 28376 return args.content; 28377 }, 28378 28379 /** 28380 * Inserts content at caret position. 28381 * 28382 * @method insertContent 28383 * @param {String} content Content to insert. 28384 */ 28385 insertContent: function(content) { 28386 this.execCommand('mceInsertContent', false, content); 28387 }, 28388 28389 /** 28390 * Returns true/false if the editor is dirty or not. It will get dirty if the user has made modifications to the contents. 28391 * 28392 * @method isDirty 28393 * @return {Boolean} True/false if the editor is dirty or not. It will get dirty if the user has made modifications to the contents. 28394 * @example 28395 * if (tinymce.activeEditor.isDirty()) 28396 * alert("You must save your contents."); 28397 */ 28398 isDirty: function() { 28399 return !this.isNotDirty; 28400 }, 28401 28402 /** 28403 * Returns the editors container element. The container element wrappes in 28404 * all the elements added to the page for the editor. Such as UI, iframe etc. 28405 * 28406 * @method getContainer 28407 * @return {Element} HTML DOM element for the editor container. 28408 */ 28409 getContainer: function() { 28410 var self = this; 28411 28412 if (!self.container) { 28413 self.container = DOM.get(self.editorContainer || self.id + '_parent'); 28414 } 28415 28416 return self.container; 28417 }, 28418 28419 /** 28420 * Returns the editors content area container element. The this element is the one who 28421 * holds the iframe or the editable element. 28422 * 28423 * @method getContentAreaContainer 28424 * @return {Element} HTML DOM element for the editor area container. 28425 */ 28426 getContentAreaContainer: function() { 28427 return this.contentAreaContainer; 28428 }, 28429 28430 /** 28431 * Returns the target element/textarea that got replaced with a TinyMCE editor instance. 28432 * 28433 * @method getElement 28434 * @return {Element} HTML DOM element for the replaced element. 28435 */ 28436 getElement: function() { 28437 return DOM.get(this.settings.content_element || this.id); 28438 }, 28439 28440 /** 28441 * Returns the iframes window object. 28442 * 28443 * @method getWin 28444 * @return {Window} Iframe DOM window object. 28445 */ 28446 getWin: function() { 28447 var self = this, elm; 28448 28449 if (!self.contentWindow) { 28450 elm = DOM.get(self.id + "_ifr"); 28451 28452 if (elm) { 28453 self.contentWindow = elm.contentWindow; 28454 } 28455 } 28456 28457 return self.contentWindow; 28458 }, 28459 28460 /** 28461 * Returns the iframes document object. 28462 * 28463 * @method getDoc 28464 * @return {Document} Iframe DOM document object. 28465 */ 28466 getDoc: function() { 28467 var self = this, win; 28468 28469 if (!self.contentDocument) { 28470 win = self.getWin(); 28471 28472 if (win) { 28473 self.contentDocument = win.document; 28474 } 28475 } 28476 28477 return self.contentDocument; 28478 }, 28479 28480 /** 28481 * Returns the iframes body element. 28482 * 28483 * @method getBody 28484 * @return {Element} Iframe body element. 28485 */ 28486 getBody: function() { 28487 return this.bodyElement || this.getDoc().body; 28488 }, 28489 28490 /** 28491 * URL converter function this gets executed each time a user adds an img, a or 28492 * any other element that has a URL in it. This will be called both by the DOM and HTML 28493 * manipulation functions. 28494 * 28495 * @method convertURL 28496 * @param {string} url URL to convert. 28497 * @param {string} name Attribute name src, href etc. 28498 * @param {string/HTMLElement} elm Tag name or HTML DOM element depending on HTML or DOM insert. 28499 * @return {string} Converted URL string. 28500 */ 28501 convertURL: function(url, name, elm) { 28502 var self = this, settings = self.settings; 28503 28504 // Use callback instead 28505 if (settings.urlconverter_callback) { 28506 return self.execCallback('urlconverter_callback', url, elm, true, name); 28507 } 28508 28509 // Don't convert link href since thats the CSS files that gets loaded into the editor also skip local file URLs 28510 if (!settings.convert_urls || (elm && elm.nodeName == 'LINK') || url.indexOf('file:') === 0 || url.length === 0) { 28511 return url; 28512 } 28513 28514 // Convert to relative 28515 if (settings.relative_urls) { 28516 return self.documentBaseURI.toRelative(url); 28517 } 28518 28519 // Convert to absolute 28520 url = self.documentBaseURI.toAbsolute(url, settings.remove_script_host); 28521 28522 return url; 28523 }, 28524 28525 /** 28526 * Adds visual aid for tables, anchors etc so they can be more easily edited inside the editor. 28527 * 28528 * @method addVisual 28529 * @param {Element} elm Optional root element to loop though to find tables etc that needs the visual aid. 28530 */ 28531 addVisual: function(elm) { 28532 var self = this, settings = self.settings, dom = self.dom, cls; 28533 28534 elm = elm || self.getBody(); 28535 28536 if (self.hasVisual === undefined) { 28537 self.hasVisual = settings.visual; 28538 } 28539 28540 each(dom.select('table,a', elm), function(elm) { 28541 var value; 28542 28543 switch (elm.nodeName) { 28544 case 'TABLE': 28545 cls = settings.visual_table_class || 'mce-item-table'; 28546 value = dom.getAttrib(elm, 'border'); 28547 28548 if (!value || value == '0') { 28549 if (self.hasVisual) { 28550 dom.addClass(elm, cls); 28551 } else { 28552 dom.removeClass(elm, cls); 28553 } 28554 } 28555 28556 return; 28557 28558 case 'A': 28559 if (!dom.getAttrib(elm, 'href', false)) { 28560 value = dom.getAttrib(elm, 'name') || elm.id; 28561 cls = settings.visual_anchor_class || 'mce-item-anchor'; 28562 28563 if (value) { 28564 if (self.hasVisual) { 28565 dom.addClass(elm, cls); 28566 } else { 28567 dom.removeClass(elm, cls); 28568 } 28569 } 28570 } 28571 28572 return; 28573 } 28574 }); 28575 28576 self.fire('VisualAid', {element: elm, hasVisual: self.hasVisual}); 28577 }, 28578 28579 /** 28580 * Removes the editor from the dom and tinymce collection. 28581 * 28582 * @method remove 28583 */ 28584 remove: function() { 28585 var self = this; 28586 28587 if (!self.removed) { 28588 self.removed = 1; 28589 self.save(); 28590 28591 // Remove any hidden input 28592 if (self.hasHiddenInput) { 28593 DOM.remove(self.getElement().nextSibling); 28594 } 28595 28596 if (!self.inline) { 28597 // IE 9 has a bug where the selection stops working if you place the 28598 // caret inside the editor then remove the iframe 28599 if (ie && ie < 10) { 28600 self.getDoc().execCommand('SelectAll', false, null); 28601 } 28602 28603 DOM.setStyle(self.id, 'display', self.orgDisplay); 28604 self.getBody().onload = null; // Prevent #6816 28605 28606 // Don't clear the window or document if content editable 28607 // is enabled since other instances might still be present 28608 Event.unbind(self.getWin()); 28609 Event.unbind(self.getDoc()); 28610 } 28611 28612 var elm = self.getContainer(); 28613 Event.unbind(self.getBody()); 28614 Event.unbind(elm); 28615 28616 self.fire('remove'); 28617 28618 self.editorManager.remove(self); 28619 DOM.remove(elm); 28620 self.destroy(); 28621 } 28622 }, 28623 28624 /** 28625 * Destroys the editor instance by removing all events, element references or other resources 28626 * that could leak memory. This method will be called automatically when the page is unloaded 28627 * but you can also call it directly if you know what you are doing. 28628 * 28629 * @method destroy 28630 * @param {Boolean} automatic Optional state if the destroy is an automatic destroy or user called one. 28631 */ 28632 destroy: function(automatic) { 28633 var self = this, form; 28634 28635 // One time is enough 28636 if (self.destroyed) { 28637 return; 28638 } 28639 28640 // If user manually calls destroy and not remove 28641 // Users seems to have logic that calls destroy instead of remove 28642 if (!automatic && !self.removed) { 28643 self.remove(); 28644 return; 28645 } 28646 28647 // We must unbind on Gecko since it would otherwise produce the pesky "attempt 28648 // to run compile-and-go script on a cleared scope" message 28649 if (automatic && isGecko) { 28650 Event.unbind(self.getDoc()); 28651 Event.unbind(self.getWin()); 28652 Event.unbind(self.getBody()); 28653 } 28654 28655 if (!automatic) { 28656 self.editorManager.off('beforeunload', self._beforeUnload); 28657 28658 // Manual destroy 28659 if (self.theme && self.theme.destroy) { 28660 self.theme.destroy(); 28661 } 28662 28663 // Destroy controls, selection and dom 28664 self.selection.destroy(); 28665 self.dom.destroy(); 28666 } 28667 28668 form = self.formElement; 28669 if (form) { 28670 if (form._mceOldSubmit) { 28671 form.submit = form._mceOldSubmit; 28672 form._mceOldSubmit = null; 28673 } 28674 28675 DOM.unbind(form, 'submit reset', self.formEventDelegate); 28676 } 28677 28678 self.contentAreaContainer = self.formElement = self.container = self.editorContainer = null; 28679 self.settings.content_element = self.bodyElement = self.contentDocument = self.contentWindow = null; 28680 28681 if (self.selection) { 28682 self.selection = self.selection.win = self.selection.dom = self.selection.dom.doc = null; 28683 } 28684 28685 self.destroyed = 1; 28686 }, 28687 28688 // Internal functions 28689 28690 _refreshContentEditable: function() { 28691 var self = this, body, parent; 28692 28693 // Check if the editor was hidden and the re-initalize contentEditable mode by removing and adding the body again 28694 if (self._isHidden()) { 28695 body = self.getBody(); 28696 parent = body.parentNode; 28697 28698 parent.removeChild(body); 28699 parent.appendChild(body); 28700 28701 body.focus(); 28702 } 28703 }, 28704 28705 _isHidden: function() { 28706 var sel; 28707 28708 if (!isGecko) { 28709 return 0; 28710 } 28711 28712 // Weird, wheres that cursor selection? 28713 sel = this.selection.getSel(); 28714 return (!sel || !sel.rangeCount || sel.rangeCount === 0); 28715 } 28716 }; 28717 28718 extend(Editor.prototype, EditorObservable); 28719 28720 return Editor; 28721 }); 28722 28723 // Included from: js/tinymce/classes/util/I18n.js 28724 28725 /** 28726 * I18n.js 28727 * 28728 * Copyright, Moxiecode Systems AB 28729 * Released under LGPL License. 28730 * 28731 * License: http://www.tinymce.com/license 28732 * Contributing: http://www.tinymce.com/contributing 28733 */ 28734 28735 /** 28736 * I18n class that handles translation of TinyMCE UI. 28737 * Uses po style with csharp style parameters. 28738 * 28739 * @class tinymce.util.I18n 28740 */ 28741 define("tinymce/util/I18n", [], function() { 28742 "use strict"; 28743 28744 var data = {}; 28745 28746 return { 28747 /** 28748 * Property gets set to true if a RTL language pack was loaded. 28749 * 28750 * @property rtl 28751 * @type Boolean 28752 */ 28753 rtl: false, 28754 28755 /** 28756 * Adds translations for a specific language code. 28757 * 28758 * @method add 28759 * @param {String} code Language code like sv_SE. 28760 * @param {Array} items Name/value array with English en_US to sv_SE. 28761 */ 28762 add: function(code, items) { 28763 for (var name in items) { 28764 data[name] = items[name]; 28765 } 28766 28767 this.rtl = this.rtl || data._dir === 'rtl'; 28768 }, 28769 28770 /** 28771 * Translates the specified text. 28772 * 28773 * It has a few formats: 28774 * I18n.translate("Text"); 28775 * I18n.translate(["Text {0}/{1}", 0, 1]); 28776 * I18n.translate({raw: "Raw string"}); 28777 * 28778 * @method translate 28779 * @param {String/Object/Array} text Text to translate. 28780 * @return {String} String that got translated. 28781 */ 28782 translate: function(text) { 28783 if (typeof(text) == "undefined") { 28784 return text; 28785 } 28786 28787 if (typeof(text) != "string" && text.raw) { 28788 return text.raw; 28789 } 28790 28791 if (text.push) { 28792 var values = text.slice(1); 28793 28794 text = (data[text[0]] || text[0]).replace(/\{([^\}]+)\}/g, function(match1, match2) { 28795 return values[match2]; 28796 }); 28797 } 28798 28799 return data[text] || text; 28800 }, 28801 28802 data: data 28803 }; 28804 }); 28805 28806 // Included from: js/tinymce/classes/FocusManager.js 28807 28808 /** 28809 * FocusManager.js 28810 * 28811 * Copyright, Moxiecode Systems AB 28812 * Released under LGPL License. 28813 * 28814 * License: http://www.tinymce.com/license 28815 * Contributing: http://www.tinymce.com/contributing 28816 */ 28817 28818 /** 28819 * This class manages the focus/blur state of the editor. This class is needed since some 28820 * browsers fire false focus/blur states when the selection is moved to a UI dialog or similar. 28821 * 28822 * This class will fire two events focus and blur on the editor instances that got affected. 28823 * It will also handle the restore of selection when the focus is lost and returned. 28824 * 28825 * @class tinymce.FocusManager 28826 */ 28827 define("tinymce/FocusManager", [ 28828 "tinymce/dom/DOMUtils", 28829 "tinymce/Env" 28830 ], function(DOMUtils, Env) { 28831 var selectionChangeHandler, documentFocusInHandler, documentMouseUpHandler, DOM = DOMUtils.DOM; 28832 28833 /** 28834 * Constructs a new focus manager instance. 28835 * 28836 * @constructor FocusManager 28837 * @param {tinymce.EditorManager} editorManager Editor manager instance to handle focus for. 28838 */ 28839 function FocusManager(editorManager) { 28840 function getActiveElement() { 28841 try { 28842 return document.activeElement; 28843 } catch (ex) { 28844 // IE sometimes fails to get the activeElement when resizing table 28845 // TODO: Investigate this 28846 return document.body; 28847 } 28848 } 28849 28850 // We can't store a real range on IE 11 since it gets mutated so we need to use a bookmark object 28851 // TODO: Move this to a separate range utils class since it's it's logic is present in Selection as well. 28852 function createBookmark(dom, rng) { 28853 if (rng && rng.startContainer) { 28854 // Verify that the range is within the root of the editor 28855 if (!dom.isChildOf(rng.startContainer, dom.getRoot()) || !dom.isChildOf(rng.endContainer, dom.getRoot())) { 28856 return; 28857 } 28858 28859 return { 28860 startContainer: rng.startContainer, 28861 startOffset: rng.startOffset, 28862 endContainer: rng.endContainer, 28863 endOffset: rng.endOffset 28864 }; 28865 } 28866 28867 return rng; 28868 } 28869 28870 function bookmarkToRng(editor, bookmark) { 28871 var rng; 28872 28873 if (bookmark.startContainer) { 28874 rng = editor.getDoc().createRange(); 28875 rng.setStart(bookmark.startContainer, bookmark.startOffset); 28876 rng.setEnd(bookmark.endContainer, bookmark.endOffset); 28877 } else { 28878 rng = bookmark; 28879 } 28880 28881 return rng; 28882 } 28883 28884 function isUIElement(elm) { 28885 return !!DOM.getParent(elm, FocusManager.isEditorUIElement); 28886 } 28887 28888 function registerEvents(e) { 28889 var editor = e.editor; 28890 28891 editor.on('init', function() { 28892 // Gecko/WebKit has ghost selections in iframes and IE only has one selection per browser tab 28893 if (editor.inline || Env.ie) { 28894 // On other browsers take snapshot on nodechange in inline mode since they have Ghost selections for iframes 28895 editor.on('nodechange keyup', function() { 28896 var node = document.activeElement; 28897 28898 // IE 11 reports active element as iframe not body of iframe 28899 if (node && node.id == editor.id + '_ifr') { 28900 node = editor.getBody(); 28901 } 28902 28903 if (editor.dom.isChildOf(node, editor.getBody())) { 28904 editor.lastRng = editor.selection.getRng(); 28905 } 28906 }); 28907 28908 // Handles the issue with WebKit not retaining selection within inline document 28909 // If the user releases the mouse out side the body since a mouse up event wont occur on the body 28910 if (Env.webkit && !selectionChangeHandler) { 28911 selectionChangeHandler = function() { 28912 var activeEditor = editorManager.activeEditor; 28913 28914 if (activeEditor && activeEditor.selection) { 28915 var rng = activeEditor.selection.getRng(); 28916 28917 // Store when it's non collapsed 28918 if (rng && !rng.collapsed) { 28919 editor.lastRng = rng; 28920 } 28921 } 28922 }; 28923 28924 DOM.bind(document, 'selectionchange', selectionChangeHandler); 28925 } 28926 } 28927 }); 28928 28929 editor.on('setcontent', function() { 28930 editor.lastRng = null; 28931 }); 28932 28933 // Remove last selection bookmark on mousedown see #6305 28934 editor.on('mousedown', function() { 28935 editor.selection.lastFocusBookmark = null; 28936 }); 28937 28938 editor.on('focusin', function() { 28939 var focusedEditor = editorManager.focusedEditor; 28940 28941 if (editor.selection.lastFocusBookmark) { 28942 editor.selection.setRng(bookmarkToRng(editor, editor.selection.lastFocusBookmark)); 28943 editor.selection.lastFocusBookmark = null; 28944 } 28945 28946 if (focusedEditor != editor) { 28947 if (focusedEditor) { 28948 focusedEditor.fire('blur', {focusedEditor: editor}); 28949 } 28950 28951 editorManager.activeEditor = editor; 28952 editorManager.focusedEditor = editor; 28953 editor.fire('focus', {blurredEditor: focusedEditor}); 28954 editor.focus(true); 28955 } 28956 28957 editor.lastRng = null; 28958 }); 28959 28960 editor.on('focusout', function() { 28961 window.setTimeout(function() { 28962 var focusedEditor = editorManager.focusedEditor; 28963 28964 // Still the same editor the the blur was outside any editor UI 28965 if (!isUIElement(getActiveElement()) && focusedEditor == editor) { 28966 editor.fire('blur', {focusedEditor: null}); 28967 editorManager.focusedEditor = null; 28968 28969 // Make sure selection is valid could be invalid if the editor is blured and removed before the timeout occurs 28970 if (editor.selection) { 28971 editor.selection.lastFocusBookmark = null; 28972 } 28973 } 28974 }, 0); 28975 }); 28976 28977 // Check if focus is moved to an element outside the active editor by checking if the target node 28978 // isn't within the body of the activeEditor nor a UI element such as a dialog child control 28979 if (!documentFocusInHandler) { 28980 documentFocusInHandler = function(e) { 28981 var activeEditor = editorManager.activeEditor; 28982 28983 if (activeEditor && e.target.ownerDocument == document) { 28984 // Check to make sure we have a valid selection 28985 if (activeEditor.selection) { 28986 activeEditor.selection.lastFocusBookmark = createBookmark(activeEditor.dom, activeEditor.lastRng); 28987 } 28988 28989 // Fire a blur event if the element isn't a UI element 28990 if (!isUIElement(e.target) && editorManager.focusedEditor == activeEditor) { 28991 activeEditor.fire('blur', {focusedEditor: null}); 28992 editorManager.focusedEditor = null; 28993 } 28994 } 28995 }; 28996 28997 DOM.bind(document, 'focusin', documentFocusInHandler); 28998 } 28999 29000 // Handle edge case when user starts the selection inside the editor and releases 29001 // the mouse outside the editor producing a new selection. This weird workaround is needed since 29002 // Gecko doesn't have the "selectionchange" event we need to do this. Fixes: #6843 29003 if (editor.inline && !documentMouseUpHandler) { 29004 documentMouseUpHandler = function(e) { 29005 var activeEditor = editorManager.activeEditor; 29006 29007 if (activeEditor.inline && !activeEditor.dom.isChildOf(e.target, activeEditor.getBody())) { 29008 var rng = activeEditor.selection.getRng(); 29009 29010 if (!rng.collapsed) { 29011 activeEditor.lastRng = rng; 29012 } 29013 } 29014 }; 29015 29016 DOM.bind(document, 'mouseup', documentMouseUpHandler); 29017 } 29018 } 29019 29020 function unregisterDocumentEvents(e) { 29021 if (editorManager.focusedEditor == e.editor) { 29022 editorManager.focusedEditor = null; 29023 } 29024 29025 if (!editorManager.activeEditor) { 29026 DOM.unbind(document, 'selectionchange', selectionChangeHandler); 29027 DOM.unbind(document, 'focusin', documentFocusInHandler); 29028 DOM.unbind(document, 'mouseup', documentMouseUpHandler); 29029 selectionChangeHandler = documentFocusInHandler = documentMouseUpHandler = null; 29030 } 29031 } 29032 29033 editorManager.on('AddEditor', registerEvents); 29034 editorManager.on('RemoveEditor', unregisterDocumentEvents); 29035 } 29036 29037 /** 29038 * Returns true if the specified element is part of the UI for example an button or text input. 29039 * 29040 * @method isEditorUIElement 29041 * @param {Element} elm Element to check if it's part of the UI or not. 29042 * @return {Boolean} True/false state if the element is part of the UI or not. 29043 */ 29044 FocusManager.isEditorUIElement = function(elm) { 29045 // Needs to be converted to string since svg can have focus: #6776 29046 return elm.className.toString().indexOf('mce-') !== -1; 29047 }; 29048 29049 return FocusManager; 29050 }); 29051 29052 // Included from: js/tinymce/classes/EditorManager.js 29053 29054 /** 29055 * EditorManager.js 29056 * 29057 * Copyright, Moxiecode Systems AB 29058 * Released under LGPL License. 29059 * 29060 * License: http://www.tinymce.com/license 29061 * Contributing: http://www.tinymce.com/contributing 29062 */ 29063 29064 /** 29065 * This class used as a factory for manager for tinymce.Editor instances. 29066 * 29067 * @example 29068 * tinymce.EditorManager.init({}); 29069 * 29070 * @class tinymce.EditorManager 29071 * @mixes tinymce.util.Observable 29072 * @static 29073 */ 29074 define("tinymce/EditorManager", [ 29075 "tinymce/Editor", 29076 "tinymce/dom/DOMUtils", 29077 "tinymce/util/URI", 29078 "tinymce/Env", 29079 "tinymce/util/Tools", 29080 "tinymce/util/Observable", 29081 "tinymce/util/I18n", 29082 "tinymce/FocusManager" 29083 ], function(Editor, DOMUtils, URI, Env, Tools, Observable, I18n, FocusManager) { 29084 var DOM = DOMUtils.DOM; 29085 var explode = Tools.explode, each = Tools.each, extend = Tools.extend; 29086 var instanceCounter = 0, beforeUnloadDelegate, EditorManager; 29087 29088 function removeEditorFromList(editor) { 29089 var editors = EditorManager.editors, removedFromList; 29090 29091 delete editors[editor.id]; 29092 29093 for (var i = 0; i < editors.length; i++) { 29094 if (editors[i] == editor) { 29095 editors.splice(i, 1); 29096 removedFromList = true; 29097 break; 29098 } 29099 } 29100 29101 // Select another editor since the active one was removed 29102 if (EditorManager.activeEditor == editor) { 29103 EditorManager.activeEditor = editors[0]; 29104 } 29105 29106 // Clear focusedEditor if necessary, so that we don't try to blur the destroyed editor 29107 if (EditorManager.focusedEditor == editor) { 29108 EditorManager.focusedEditor = null; 29109 } 29110 29111 return removedFromList; 29112 } 29113 29114 function purgeDestroyedEditor(editor) { 29115 // User has manually destroyed the editor lets clean up the mess 29116 if (editor && !(editor.getContainer() || editor.getBody()).parentNode) { 29117 removeEditorFromList(editor); 29118 editor.destroy(true); 29119 editor = null; 29120 } 29121 29122 return editor; 29123 } 29124 29125 EditorManager = { 29126 /** 29127 * Major version of TinyMCE build. 29128 * 29129 * @property majorVersion 29130 * @type String 29131 */ 29132 majorVersion : '4', 29133 29134 /** 29135 * Minor version of TinyMCE build. 29136 * 29137 * @property minorVersion 29138 * @type String 29139 */ 29140 minorVersion : '0.25', 29141 29142 /** 29143 * Release date of TinyMCE build. 29144 * 29145 * @property releaseDate 29146 * @type String 29147 */ 29148 releaseDate: '2014-04-30', 29149 29150 /** 29151 * Collection of editor instances. 29152 * 29153 * @property editors 29154 * @type Object 29155 * @example 29156 * for (edId in tinymce.editors) 29157 * tinymce.editors[edId].save(); 29158 */ 29159 editors: [], 29160 29161 /** 29162 * Collection of language pack data. 29163 * 29164 * @property i18n 29165 * @type Object 29166 */ 29167 i18n: I18n, 29168 29169 /** 29170 * Currently active editor instance. 29171 * 29172 * @property activeEditor 29173 * @type tinymce.Editor 29174 * @example 29175 * tinyMCE.activeEditor.selection.getContent(); 29176 * tinymce.EditorManager.activeEditor.selection.getContent(); 29177 */ 29178 activeEditor: null, 29179 29180 setup: function() { 29181 var self = this, baseURL, documentBaseURL, suffix = "", preInit, src; 29182 29183 // Get base URL for the current document 29184 documentBaseURL = document.location.href.replace(/[\?#].*$/, '').replace(/[\/\\][^\/]+$/, ''); 29185 if (!/[\/\\]$/.test(documentBaseURL)) { 29186 documentBaseURL += '/'; 29187 } 29188 29189 // If tinymce is defined and has a base use that or use the old tinyMCEPreInit 29190 preInit = window.tinymce || window.tinyMCEPreInit; 29191 if (preInit) { 29192 baseURL = preInit.base || preInit.baseURL; 29193 suffix = preInit.suffix; 29194 } else { 29195 // Get base where the tinymce script is located 29196 var scripts = document.getElementsByTagName('script'); 29197 for (var i = 0; i < scripts.length; i++) { 29198 src = scripts[i].src; 29199 29200 // Script types supported: 29201 // tinymce.js tinymce.min.js tinymce.dev.js 29202 // tinymce.jquery.js tinymce.jquery.min.js tinymce.jquery.dev.js 29203 // tinymce.full.js tinymce.full.min.js tinymce.full.dev.js 29204 if (/tinymce(\.full|\.jquery|)(\.min|\.dev|)\.js/.test(src)) { 29205 if (src.indexOf('.min') != -1) { 29206 suffix = '.min'; 29207 } 29208 29209 baseURL = src.substring(0, src.lastIndexOf('/')); 29210 break; 29211 } 29212 } 29213 29214 // We didn't find any baseURL by looking at the script elements 29215 // Try to use the document.currentScript as a fallback 29216 if (!baseURL && document.currentScript) { 29217 src = document.currentScript.src; 29218 29219 if (src.indexOf('.min') != -1) { 29220 suffix = '.min'; 29221 } 29222 29223 baseURL = src.substring(0, src.lastIndexOf('/')); 29224 } 29225 } 29226 29227 /** 29228 * Base URL where the root directory if TinyMCE is located. 29229 * 29230 * @property baseURL 29231 * @type String 29232 */ 29233 self.baseURL = new URI(documentBaseURL).toAbsolute(baseURL); 29234 29235 /** 29236 * Document base URL where the current document is located. 29237 * 29238 * @property documentBaseURL 29239 * @type String 29240 */ 29241 self.documentBaseURL = documentBaseURL; 29242 29243 /** 29244 * Absolute baseURI for the installation path of TinyMCE. 29245 * 29246 * @property baseURI 29247 * @type tinymce.util.URI 29248 */ 29249 self.baseURI = new URI(self.baseURL); 29250 29251 /** 29252 * Current suffix to add to each plugin/theme that gets loaded for example ".min". 29253 * 29254 * @property suffix 29255 * @type String 29256 */ 29257 self.suffix = suffix; 29258 29259 self.focusManager = new FocusManager(self); 29260 }, 29261 29262 /** 29263 * Initializes a set of editors. This method will create editors based on various settings. 29264 * 29265 * @method init 29266 * @param {Object} settings Settings object to be passed to each editor instance. 29267 * @example 29268 * // Initializes a editor using the longer method 29269 * tinymce.EditorManager.init({ 29270 * some_settings : 'some value' 29271 * }); 29272 * 29273 * // Initializes a editor instance using the shorter version 29274 * tinyMCE.init({ 29275 * some_settings : 'some value' 29276 * }); 29277 */ 29278 init: function(settings) { 29279 var self = this, editors = [], editor; 29280 29281 function createId(elm) { 29282 var id = elm.id; 29283 29284 // Use element id, or unique name or generate a unique id 29285 if (!id) { 29286 id = elm.name; 29287 29288 if (id && !DOM.get(id)) { 29289 id = elm.name; 29290 } else { 29291 // Generate unique name 29292 id = DOM.uniqueId(); 29293 } 29294 29295 elm.setAttribute('id', id); 29296 } 29297 29298 return id; 29299 } 29300 29301 function createEditor(id, settings) { 29302 if (!purgeDestroyedEditor(self.get(id))) { 29303 var editor = new Editor(id, settings, self); 29304 editors.push(editor); 29305 editor.render(); 29306 } 29307 } 29308 29309 function execCallback(se, n, s) { 29310 var f = se[n]; 29311 29312 if (!f) { 29313 return; 29314 } 29315 29316 return f.apply(s || this, Array.prototype.slice.call(arguments, 2)); 29317 } 29318 29319 function hasClass(n, c) { 29320 return c.constructor === RegExp ? c.test(n.className) : DOM.hasClass(n, c); 29321 } 29322 29323 function readyHandler() { 29324 var l, co; 29325 29326 DOM.unbind(window, 'ready', readyHandler); 29327 29328 execCallback(settings, 'onpageload'); 29329 29330 if (settings.types) { 29331 // Process type specific selector 29332 each(settings.types, function(type) { 29333 each(DOM.select(type.selector), function(elm) { 29334 createEditor(createId(elm), extend({}, settings, type)); 29335 }); 29336 }); 29337 29338 return; 29339 } else if (settings.selector) { 29340 // Process global selector 29341 each(DOM.select(settings.selector), function(elm) { 29342 createEditor(createId(elm), settings); 29343 }); 29344 29345 return; 29346 } 29347 29348 // Fallback to old setting 29349 switch (settings.mode) { 29350 case "exact": 29351 l = settings.elements || ''; 29352 29353 if (l.length > 0) { 29354 each(explode(l), function(v) { 29355 if (DOM.get(v)) { 29356 editor = new Editor(v, settings, self); 29357 editors.push(editor); 29358 editor.render(); 29359 } else { 29360 each(document.forms, function(f) { 29361 each(f.elements, function(e) { 29362 if (e.name === v) { 29363 v = 'mce_editor_' + instanceCounter++; 29364 DOM.setAttrib(e, 'id', v); 29365 createEditor(v, settings); 29366 } 29367 }); 29368 }); 29369 } 29370 }); 29371 } 29372 break; 29373 29374 case "textareas": 29375 case "specific_textareas": 29376 each(DOM.select('textarea'), function(elm) { 29377 if (settings.editor_deselector && hasClass(elm, settings.editor_deselector)) { 29378 return; 29379 } 29380 29381 if (!settings.editor_selector || hasClass(elm, settings.editor_selector)) { 29382 createEditor(createId(elm), settings); 29383 } 29384 }); 29385 break; 29386 } 29387 29388 // Call onInit when all editors are initialized 29389 if (settings.oninit) { 29390 l = co = 0; 29391 29392 each(editors, function(ed) { 29393 co++; 29394 29395 if (!ed.initialized) { 29396 // Wait for it 29397 ed.on('init', function() { 29398 l++; 29399 29400 // All done 29401 if (l == co) { 29402 execCallback(settings, 'oninit'); 29403 } 29404 }); 29405 } else { 29406 l++; 29407 } 29408 29409 // All done 29410 if (l == co) { 29411 execCallback(settings, 'oninit'); 29412 } 29413 }); 29414 } 29415 } 29416 29417 self.settings = settings; 29418 29419 DOM.bind(window, 'ready', readyHandler); 29420 }, 29421 29422 /** 29423 * Returns a editor instance by id. 29424 * 29425 * @method get 29426 * @param {String/Number} id Editor instance id or index to return. 29427 * @return {tinymce.Editor} Editor instance to return. 29428 * @example 29429 * // Adds an onclick event to an editor by id (shorter version) 29430 * tinymce.get('mytextbox').on('click', function(e) { 29431 * ed.windowManager.alert('Hello world!'); 29432 * }); 29433 * 29434 * // Adds an onclick event to an editor by id (longer version) 29435 * tinymce.EditorManager.get('mytextbox').on('click', function(e) { 29436 * ed.windowManager.alert('Hello world!'); 29437 * }); 29438 */ 29439 get: function(id) { 29440 if (!arguments.length) { 29441 return this.editors; 29442 } 29443 29444 return id in this.editors ? this.editors[id] : null; 29445 }, 29446 29447 /** 29448 * Adds an editor instance to the editor collection. This will also set it as the active editor. 29449 * 29450 * @method add 29451 * @param {tinymce.Editor} editor Editor instance to add to the collection. 29452 * @return {tinymce.Editor} The same instance that got passed in. 29453 */ 29454 add: function(editor) { 29455 var self = this, editors = self.editors; 29456 29457 // Add named and index editor instance 29458 editors[editor.id] = editor; 29459 editors.push(editor); 29460 29461 self.activeEditor = editor; 29462 29463 /** 29464 * Fires when an editor is added to the EditorManager collection. 29465 * 29466 * @event AddEditor 29467 * @param {Object} e Event arguments. 29468 */ 29469 self.fire('AddEditor', {editor: editor}); 29470 29471 if (!beforeUnloadDelegate) { 29472 beforeUnloadDelegate = function() { 29473 self.fire('BeforeUnload'); 29474 }; 29475 29476 DOM.bind(window, 'beforeunload', beforeUnloadDelegate); 29477 } 29478 29479 return editor; 29480 }, 29481 29482 /** 29483 * Creates an editor instance and adds it to the EditorManager collection. 29484 * 29485 * @method createEditor 29486 * @param {String} id Instance id to use for editor. 29487 * @param {Object} settings Editor instance settings. 29488 * @return {tinymce.Editor} Editor instance that got created. 29489 */ 29490 createEditor: function(id, settings) { 29491 return this.add(new Editor(id, settings, this)); 29492 }, 29493 29494 /** 29495 * Removes a editor or editors form page. 29496 * 29497 * @example 29498 * // Remove all editors bound to divs 29499 * tinymce.remove('div'); 29500 * 29501 * // Remove all editors bound to textareas 29502 * tinymce.remove('textarea'); 29503 * 29504 * // Remove all editors 29505 * tinymce.remove(); 29506 * 29507 * // Remove specific instance by id 29508 * tinymce.remove('#id'); 29509 * 29510 * @method remove 29511 * @param {tinymce.Editor/String/Object} [selector] CSS selector or editor instance to remove. 29512 * @return {tinymce.Editor} The editor that got passed in will be return if it was found otherwise null. 29513 */ 29514 remove: function(selector) { 29515 var self = this, i, editors = self.editors, editor; 29516 29517 // Remove all editors 29518 if (!selector) { 29519 for (i = editors.length - 1; i >= 0; i--) { 29520 self.remove(editors[i]); 29521 } 29522 29523 return; 29524 } 29525 29526 // Remove editors by selector 29527 if (typeof(selector) == "string") { 29528 selector = selector.selector || selector; 29529 29530 each(DOM.select(selector), function(elm) { 29531 self.remove(editors[elm.id]); 29532 }); 29533 29534 return; 29535 } 29536 29537 // Remove specific editor 29538 editor = selector; 29539 29540 // Not in the collection 29541 if (!editors[editor.id]) { 29542 return null; 29543 } 29544 29545 /** 29546 * Fires when an editor is removed from EditorManager collection. 29547 * 29548 * @event RemoveEditor 29549 * @param {Object} e Event arguments. 29550 */ 29551 if (removeEditorFromList(editor)) { 29552 self.fire('RemoveEditor', {editor: editor}); 29553 } 29554 29555 if (!editors.length) { 29556 DOM.unbind(window, 'beforeunload', beforeUnloadDelegate); 29557 } 29558 29559 editor.remove(); 29560 29561 return editor; 29562 }, 29563 29564 /** 29565 * Executes a specific command on the currently active editor. 29566 * 29567 * @method execCommand 29568 * @param {String} c Command to perform for example Bold. 29569 * @param {Boolean} u Optional boolean state if a UI should be presented for the command or not. 29570 * @param {String} v Optional value parameter like for example an URL to a link. 29571 * @return {Boolean} true/false if the command was executed or not. 29572 */ 29573 execCommand: function(cmd, ui, value) { 29574 var self = this, editor = self.get(value); 29575 29576 // Manager commands 29577 switch (cmd) { 29578 case "mceAddEditor": 29579 if (!self.get(value)) { 29580 new Editor(value, self.settings, self).render(); 29581 } 29582 29583 return true; 29584 29585 case "mceRemoveEditor": 29586 if (editor) { 29587 editor.remove(); 29588 } 29589 29590 return true; 29591 29592 case 'mceToggleEditor': 29593 if (!editor) { 29594 self.execCommand('mceAddEditor', 0, value); 29595 return true; 29596 } 29597 29598 if (editor.isHidden()) { 29599 editor.show(); 29600 } else { 29601 editor.hide(); 29602 } 29603 29604 return true; 29605 } 29606 29607 // Run command on active editor 29608 if (self.activeEditor) { 29609 return self.activeEditor.execCommand(cmd, ui, value); 29610 } 29611 29612 return false; 29613 }, 29614 29615 /** 29616 * Calls the save method on all editor instances in the collection. This can be useful when a form is to be submitted. 29617 * 29618 * @method triggerSave 29619 * @example 29620 * // Saves all contents 29621 * tinyMCE.triggerSave(); 29622 */ 29623 triggerSave: function() { 29624 each(this.editors, function(editor) { 29625 editor.save(); 29626 }); 29627 }, 29628 29629 /** 29630 * Adds a language pack, this gets called by the loaded language files like en.js. 29631 * 29632 * @method addI18n 29633 * @param {String} code Optional language code. 29634 * @param {Object} items Name/value object with translations. 29635 */ 29636 addI18n: function(code, items) { 29637 I18n.add(code, items); 29638 }, 29639 29640 /** 29641 * Translates the specified string using the language pack items. 29642 * 29643 * @method translate 29644 * @param {String/Array/Object} text String to translate 29645 * @return {String} Translated string. 29646 */ 29647 translate: function(text) { 29648 return I18n.translate(text); 29649 } 29650 }; 29651 29652 extend(EditorManager, Observable); 29653 29654 EditorManager.setup(); 29655 29656 // Export EditorManager as tinymce/tinymce in global namespace 29657 window.tinymce = window.tinyMCE = EditorManager; 29658 29659 return EditorManager; 29660 }); 29661 29662 // Included from: js/tinymce/classes/LegacyInput.js 29663 29664 /** 29665 * LegacyInput.js 29666 * 29667 * Copyright, Moxiecode Systems AB 29668 * Released under LGPL License. 29669 * 29670 * License: http://www.tinymce.com/license 29671 * Contributing: http://www.tinymce.com/contributing 29672 */ 29673 29674 define("tinymce/LegacyInput", [ 29675 "tinymce/EditorManager", 29676 "tinymce/util/Tools" 29677 ], function(EditorManager, Tools) { 29678 var each = Tools.each, explode = Tools.explode; 29679 29680 EditorManager.on('AddEditor', function(e) { 29681 var editor = e.editor; 29682 29683 editor.on('preInit', function() { 29684 var filters, fontSizes, dom, settings = editor.settings; 29685 29686 function replaceWithSpan(node, styles) { 29687 each(styles, function(value, name) { 29688 if (value) { 29689 dom.setStyle(node, name, value); 29690 } 29691 }); 29692 29693 dom.rename(node, 'span'); 29694 } 29695 29696 function convert(e) { 29697 dom = editor.dom; 29698 29699 if (settings.convert_fonts_to_spans) { 29700 each(dom.select('font,u,strike', e.node), function(node) { 29701 filters[node.nodeName.toLowerCase()](dom, node); 29702 }); 29703 } 29704 } 29705 29706 if (settings.inline_styles) { 29707 fontSizes = explode(settings.font_size_legacy_values); 29708 29709 filters = { 29710 font: function(dom, node) { 29711 replaceWithSpan(node, { 29712 backgroundColor: node.style.backgroundColor, 29713 color: node.color, 29714 fontFamily: node.face, 29715 fontSize: fontSizes[parseInt(node.size, 10) - 1] 29716 }); 29717 }, 29718 29719 u: function(dom, node) { 29720 replaceWithSpan(node, { 29721 textDecoration: 'underline' 29722 }); 29723 }, 29724 29725 strike: function(dom, node) { 29726 replaceWithSpan(node, { 29727 textDecoration: 'line-through' 29728 }); 29729 } 29730 }; 29731 29732 editor.on('PreProcess SetContent', convert); 29733 } 29734 }); 29735 }); 29736 }); 29737 29738 // Included from: js/tinymce/classes/util/XHR.js 29739 29740 /** 29741 * XHR.js 29742 * 29743 * Copyright, Moxiecode Systems AB 29744 * Released under LGPL License. 29745 * 29746 * License: http://www.tinymce.com/license 29747 * Contributing: http://www.tinymce.com/contributing 29748 */ 29749 29750 /** 29751 * This class enables you to send XMLHTTPRequests cross browser. 29752 * @class tinymce.util.XHR 29753 * @static 29754 * @example 29755 * // Sends a low level Ajax request 29756 * tinymce.util.XHR.send({ 29757 * url: 'someurl', 29758 * success: function(text) { 29759 * console.debug(text); 29760 * } 29761 * }); 29762 */ 29763 define("tinymce/util/XHR", [], function() { 29764 return { 29765 /** 29766 * Sends a XMLHTTPRequest. 29767 * Consult the Wiki for details on what settings this method takes. 29768 * 29769 * @method send 29770 * @param {Object} settings Object will target URL, callbacks and other info needed to make the request. 29771 */ 29772 send: function(settings) { 29773 var xhr, count = 0; 29774 29775 function ready() { 29776 if (!settings.async || xhr.readyState == 4 || count++ > 10000) { 29777 if (settings.success && count < 10000 && xhr.status == 200) { 29778 settings.success.call(settings.success_scope, '' + xhr.responseText, xhr, settings); 29779 } else if (settings.error) { 29780 settings.error.call(settings.error_scope, count > 10000 ? 'TIMED_OUT' : 'GENERAL', xhr, settings); 29781 } 29782 29783 xhr = null; 29784 } else { 29785 setTimeout(ready, 10); 29786 } 29787 } 29788 29789 // Default settings 29790 settings.scope = settings.scope || this; 29791 settings.success_scope = settings.success_scope || settings.scope; 29792 settings.error_scope = settings.error_scope || settings.scope; 29793 settings.async = settings.async === false ? false : true; 29794 settings.data = settings.data || ''; 29795 29796 xhr = new XMLHttpRequest(); 29797 29798 if (xhr) { 29799 if (xhr.overrideMimeType) { 29800 xhr.overrideMimeType(settings.content_type); 29801 } 29802 29803 xhr.open(settings.type || (settings.data ? 'POST' : 'GET'), settings.url, settings.async); 29804 29805 if (settings.content_type) { 29806 xhr.setRequestHeader('Content-Type', settings.content_type); 29807 } 29808 29809 xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); 29810 29811 xhr.send(settings.data); 29812 29813 // Syncronous request 29814 if (!settings.async) { 29815 return ready(); 29816 } 29817 29818 // Wait for response, onReadyStateChange can not be used since it leaks memory in IE 29819 setTimeout(ready, 10); 29820 } 29821 } 29822 }; 29823 }); 29824 29825 // Included from: js/tinymce/classes/util/JSON.js 29826 29827 /** 29828 * JSON.js 29829 * 29830 * Copyright, Moxiecode Systems AB 29831 * Released under LGPL License. 29832 * 29833 * License: http://www.tinymce.com/license 29834 * Contributing: http://www.tinymce.com/contributing 29835 */ 29836 29837 /** 29838 * JSON parser and serializer class. 29839 * 29840 * @class tinymce.util.JSON 29841 * @static 29842 * @example 29843 * // JSON parse a string into an object 29844 * var obj = tinymce.util.JSON.parse(somestring); 29845 * 29846 * // JSON serialize a object into an string 29847 * var str = tinymce.util.JSON.serialize(obj); 29848 */ 29849 define("tinymce/util/JSON", [], function() { 29850 function serialize(o, quote) { 29851 var i, v, t, name; 29852 29853 quote = quote || '"'; 29854 29855 if (o === null) { 29856 return 'null'; 29857 } 29858 29859 t = typeof o; 29860 29861 if (t == 'string') { 29862 v = '\bb\tt\nn\ff\rr\""\'\'\\\\'; 29863 29864 return quote + o.replace(/([\u0080-\uFFFF\x00-\x1f\"\'\\])/g, function(a, b) { 29865 // Make sure single quotes never get encoded inside double quotes for JSON compatibility 29866 if (quote === '"' && a === "'") { 29867 return a; 29868 } 29869 29870 i = v.indexOf(b); 29871 29872 if (i + 1) { 29873 return '\\' + v.charAt(i + 1); 29874 } 29875 29876 a = b.charCodeAt().toString(16); 29877 29878 return '\\u' + '0000'.substring(a.length) + a; 29879 }) + quote; 29880 } 29881 29882 if (t == 'object') { 29883 if (o.hasOwnProperty && Object.prototype.toString.call(o) === '[object Array]') { 29884 for (i = 0, v = '['; i < o.length; i++) { 29885 v += (i > 0 ? ',' : '') + serialize(o[i], quote); 29886 } 29887 29888 return v + ']'; 29889 } 29890 29891 v = '{'; 29892 29893 for (name in o) { 29894 if (o.hasOwnProperty(name)) { 29895 v += typeof o[name] != 'function' ? (v.length > 1 ? ',' + quote : quote) + name + 29896 quote + ':' + serialize(o[name], quote) : ''; 29897 } 29898 } 29899 29900 return v + '}'; 29901 } 29902 29903 return '' + o; 29904 } 29905 29906 return { 29907 /** 29908 * Serializes the specified object as a JSON string. 29909 * 29910 * @method serialize 29911 * @param {Object} obj Object to serialize as a JSON string. 29912 * @param {String} quote Optional quote string defaults to ". 29913 * @return {string} JSON string serialized from input. 29914 */ 29915 serialize: serialize, 29916 29917 /** 29918 * Unserializes/parses the specified JSON string into a object. 29919 * 29920 * @method parse 29921 * @param {string} s JSON String to parse into a JavaScript object. 29922 * @return {Object} Object from input JSON string or undefined if it failed. 29923 */ 29924 parse: function(text) { 29925 try { 29926 // Trick uglify JS 29927 return window[String.fromCharCode(101) + 'val']('(' + text + ')'); 29928 } catch (ex) { 29929 // Ignore 29930 } 29931 } 29932 29933 /**#@-*/ 29934 }; 29935 }); 29936 29937 // Included from: js/tinymce/classes/util/JSONRequest.js 29938 29939 /** 29940 * JSONRequest.js 29941 * 29942 * Copyright, Moxiecode Systems AB 29943 * Released under LGPL License. 29944 * 29945 * License: http://www.tinymce.com/license 29946 * Contributing: http://www.tinymce.com/contributing 29947 */ 29948 29949 /** 29950 * This class enables you to use JSON-RPC to call backend methods. 29951 * 29952 * @class tinymce.util.JSONRequest 29953 * @example 29954 * var json = new tinymce.util.JSONRequest({ 29955 * url: 'somebackend.php' 29956 * }); 29957 * 29958 * // Send RPC call 1 29959 * json.send({ 29960 * method: 'someMethod1', 29961 * params: ['a', 'b'], 29962 * success: function(result) { 29963 * console.dir(result); 29964 * } 29965 * }); 29966 * 29967 * // Send RPC call 2 29968 * json.send({ 29969 * method: 'someMethod2', 29970 * params: ['a', 'b'], 29971 * success: function(result) { 29972 * console.dir(result); 29973 * } 29974 * }); 29975 */ 29976 define("tinymce/util/JSONRequest", [ 29977 "tinymce/util/JSON", 29978 "tinymce/util/XHR", 29979 "tinymce/util/Tools" 29980 ], function(JSON, XHR, Tools) { 29981 var extend = Tools.extend; 29982 29983 function JSONRequest(settings) { 29984 this.settings = extend({}, settings); 29985 this.count = 0; 29986 } 29987 29988 /** 29989 * Simple helper function to send a JSON-RPC request without the need to initialize an object. 29990 * Consult the Wiki API documentation for more details on what you can pass to this function. 29991 * 29992 * @method sendRPC 29993 * @static 29994 * @param {Object} o Call object where there are three field id, method and params this object should also contain callbacks etc. 29995 */ 29996 JSONRequest.sendRPC = function(o) { 29997 return new JSONRequest().send(o); 29998 }; 29999 30000 JSONRequest.prototype = { 30001 /** 30002 * Sends a JSON-RPC call. Consult the Wiki API documentation for more details on what you can pass to this function. 30003 * 30004 * @method send 30005 * @param {Object} args Call object where there are three field id, method and params this object should also contain callbacks etc. 30006 */ 30007 send: function(args) { 30008 var ecb = args.error, scb = args.success; 30009 30010 args = extend(this.settings, args); 30011 30012 args.success = function(c, x) { 30013 c = JSON.parse(c); 30014 30015 if (typeof(c) == 'undefined') { 30016 c = { 30017 error : 'JSON Parse error.' 30018 }; 30019 } 30020 30021 if (c.error) { 30022 ecb.call(args.error_scope || args.scope, c.error, x); 30023 } else { 30024 scb.call(args.success_scope || args.scope, c.result); 30025 } 30026 }; 30027 30028 args.error = function(ty, x) { 30029 if (ecb) { 30030 ecb.call(args.error_scope || args.scope, ty, x); 30031 } 30032 }; 30033 30034 args.data = JSON.serialize({ 30035 id: args.id || 'c' + (this.count++), 30036 method: args.method, 30037 params: args.params 30038 }); 30039 30040 // JSON content type for Ruby on rails. Bug: #1883287 30041 args.content_type = 'application/json'; 30042 30043 XHR.send(args); 30044 } 30045 }; 30046 30047 return JSONRequest; 30048 }); 30049 30050 // Included from: js/tinymce/classes/util/JSONP.js 30051 30052 /** 30053 * JSONP.js 30054 * 30055 * Copyright, Moxiecode Systems AB 30056 * Released under LGPL License. 30057 * 30058 * License: http://www.tinymce.com/license 30059 * Contributing: http://www.tinymce.com/contributing 30060 */ 30061 30062 define("tinymce/util/JSONP", [ 30063 "tinymce/dom/DOMUtils" 30064 ], function(DOMUtils) { 30065 return { 30066 callbacks: {}, 30067 count: 0, 30068 30069 send: function(settings) { 30070 var self = this, dom = DOMUtils.DOM, count = settings.count !== undefined ? settings.count : self.count; 30071 var id = 'tinymce_jsonp_' + count; 30072 30073 self.callbacks[count] = function(json) { 30074 dom.remove(id); 30075 delete self.callbacks[count]; 30076 30077 settings.callback(json); 30078 }; 30079 30080 dom.add(dom.doc.body, 'script', { 30081 id: id, 30082 src: settings.url, 30083 type: 'text/javascript' 30084 }); 30085 30086 self.count++; 30087 } 30088 }; 30089 }); 30090 30091 // Included from: js/tinymce/classes/util/LocalStorage.js 30092 30093 /** 30094 * LocalStorage.js 30095 * 30096 * Copyright, Moxiecode Systems AB 30097 * Released under LGPL License. 30098 * 30099 * License: http://www.tinymce.com/license 30100 * Contributing: http://www.tinymce.com/contributing 30101 */ 30102 30103 /** 30104 * This class will simulate LocalStorage on IE 7 and return the native version on modern browsers. 30105 * Storage is done using userData on IE 7 and a special serialization format. The format is designed 30106 * to be as small as possible by making sure that the keys and values doesn't need to be encoded. This 30107 * makes it possible to store for example HTML data. 30108 * 30109 * Storage format for userData: 30110 * <base 32 key length>,<key string>,<base 32 value length>,<value>,... 30111 * 30112 * For example this data key1=value1,key2=value2 would be: 30113 * 4,key1,6,value1,4,key2,6,value2 30114 * 30115 * @class tinymce.util.LocalStorage 30116 * @static 30117 * @version 4.0 30118 * @example 30119 * tinymce.util.LocalStorage.setItem('key', 'value'); 30120 * var value = tinymce.util.LocalStorage.getItem('key'); 30121 */ 30122 define("tinymce/util/LocalStorage", [], function() { 30123 var LocalStorage, storageElm, items, keys, userDataKey, hasOldIEDataSupport; 30124 30125 // Check for native support 30126 try { 30127 if (window.localStorage) { 30128 return localStorage; 30129 } 30130 } catch (ex) { 30131 // Ignore 30132 } 30133 30134 userDataKey = "tinymce"; 30135 storageElm = document.documentElement; 30136 hasOldIEDataSupport = !!storageElm.addBehavior; 30137 30138 if (hasOldIEDataSupport) { 30139 storageElm.addBehavior('#default#userData'); 30140 } 30141 30142 /** 30143 * Gets the keys names and updates LocalStorage.length property. Since IE7 doesn't have any getters/setters. 30144 */ 30145 function updateKeys() { 30146 keys = []; 30147 30148 for (var key in items) { 30149 keys.push(key); 30150 } 30151 30152 LocalStorage.length = keys.length; 30153 } 30154 30155 /** 30156 * Loads the userData string and parses it into the items structure. 30157 */ 30158 function load() { 30159 var key, data, value, pos = 0; 30160 30161 items = {}; 30162 30163 // localStorage can be disabled on WebKit/Gecko so make a dummy storage 30164 if (!hasOldIEDataSupport) { 30165 return; 30166 } 30167 30168 function next(end) { 30169 var value, nextPos; 30170 30171 nextPos = end !== undefined ? pos + end : data.indexOf(',', pos); 30172 if (nextPos === -1 || nextPos > data.length) { 30173 return null; 30174 } 30175 30176 value = data.substring(pos, nextPos); 30177 pos = nextPos + 1; 30178 30179 return value; 30180 } 30181 30182 storageElm.load(userDataKey); 30183 data = storageElm.getAttribute(userDataKey) || ''; 30184 30185 do { 30186 var offset = next(); 30187 if (offset === null) { 30188 break; 30189 } 30190 30191 key = next(parseInt(offset, 32) || 0); 30192 if (key !== null) { 30193 offset = next(); 30194 if (offset === null) { 30195 break; 30196 } 30197 30198 value = next(parseInt(offset, 32) || 0); 30199 30200 if (key) { 30201 items[key] = value; 30202 } 30203 } 30204 } while (key !== null); 30205 30206 updateKeys(); 30207 } 30208 30209 /** 30210 * Saves the items structure into a the userData format. 30211 */ 30212 function save() { 30213 var value, data = ''; 30214 30215 // localStorage can be disabled on WebKit/Gecko so make a dummy storage 30216 if (!hasOldIEDataSupport) { 30217 return; 30218 } 30219 30220 for (var key in items) { 30221 value = items[key]; 30222 data += (data ? ',' : '') + key.length.toString(32) + ',' + key + ',' + value.length.toString(32) + ',' + value; 30223 } 30224 30225 storageElm.setAttribute(userDataKey, data); 30226 30227 try { 30228 storageElm.save(userDataKey); 30229 } catch (ex) { 30230 // Ignore disk full 30231 } 30232 30233 updateKeys(); 30234 } 30235 30236 LocalStorage = { 30237 /** 30238 * Length of the number of items in storage. 30239 * 30240 * @property length 30241 * @type Number 30242 * @return {Number} Number of items in storage. 30243 */ 30244 //length:0, 30245 30246 /** 30247 * Returns the key name by index. 30248 * 30249 * @method key 30250 * @param {Number} index Index of key to return. 30251 * @return {String} Key value or null if it wasn't found. 30252 */ 30253 key: function(index) { 30254 return keys[index]; 30255 }, 30256 30257 /** 30258 * Returns the value if the specified key or null if it wasn't found. 30259 * 30260 * @method getItem 30261 * @param {String} key Key of item to retrive. 30262 * @return {String} Value of the specified item or null if it wasn't found. 30263 */ 30264 getItem: function(key) { 30265 return key in items ? items[key] : null; 30266 }, 30267 30268 /** 30269 * Sets the value of the specified item by it's key. 30270 * 30271 * @method setItem 30272 * @param {String} key Key of the item to set. 30273 * @param {String} value Value of the item to set. 30274 */ 30275 setItem: function(key, value) { 30276 items[key] = "" + value; 30277 save(); 30278 }, 30279 30280 /** 30281 * Removes the specified item by key. 30282 * 30283 * @method removeItem 30284 * @param {String} key Key of item to remove. 30285 */ 30286 removeItem: function(key) { 30287 delete items[key]; 30288 save(); 30289 }, 30290 30291 /** 30292 * Removes all items. 30293 * 30294 * @method clear 30295 */ 30296 clear: function() { 30297 items = {}; 30298 save(); 30299 } 30300 }; 30301 30302 load(); 30303 30304 return LocalStorage; 30305 }); 30306 30307 // Included from: js/tinymce/classes/Compat.js 30308 30309 /** 30310 * Compat.js 30311 * 30312 * Copyright, Moxiecode Systems AB 30313 * Released under LGPL License. 30314 * 30315 * License: http://www.tinymce.com/license 30316 * Contributing: http://www.tinymce.com/contributing 30317 */ 30318 30319 /** 30320 * TinyMCE core class. 30321 * 30322 * @static 30323 * @class tinymce 30324 * @borrow-members tinymce.EditorManager 30325 * @borrow-members tinymce.util.Tools 30326 */ 30327 define("tinymce/Compat", [ 30328 "tinymce/dom/DOMUtils", 30329 "tinymce/dom/EventUtils", 30330 "tinymce/dom/ScriptLoader", 30331 "tinymce/AddOnManager", 30332 "tinymce/util/Tools", 30333 "tinymce/Env" 30334 ], function(DOMUtils, EventUtils, ScriptLoader, AddOnManager, Tools, Env) { 30335 var tinymce = window.tinymce; 30336 30337 /** 30338 * @property {tinymce.dom.DOMUtils} DOM Global DOM instance. 30339 * @property {tinymce.dom.ScriptLoader} ScriptLoader Global ScriptLoader instance. 30340 * @property {tinymce.AddOnManager} PluginManager Global PluginManager instance. 30341 * @property {tinymce.AddOnManager} ThemeManager Global ThemeManager instance. 30342 */ 30343 tinymce.DOM = DOMUtils.DOM; 30344 tinymce.ScriptLoader = ScriptLoader.ScriptLoader; 30345 tinymce.PluginManager = AddOnManager.PluginManager; 30346 tinymce.ThemeManager = AddOnManager.ThemeManager; 30347 30348 tinymce.dom = tinymce.dom || {}; 30349 tinymce.dom.Event = EventUtils.Event; 30350 30351 Tools.each(Tools, function(func, key) { 30352 tinymce[key] = func; 30353 }); 30354 30355 Tools.each('isOpera isWebKit isIE isGecko isMac'.split(' '), function(name) { 30356 tinymce[name] = Env[name.substr(2).toLowerCase()]; 30357 }); 30358 30359 return {}; 30360 }); 30361 30362 // Describe the different namespaces 30363 30364 /** 30365 * Root level namespace this contains classes directly releated to the TinyMCE editor. 30366 * 30367 * @namespace tinymce 30368 */ 30369 30370 /** 30371 * Contains classes for handling the browsers DOM. 30372 * 30373 * @namespace tinymce.dom 30374 */ 30375 30376 /** 30377 * Contains html parser and serializer logic. 30378 * 30379 * @namespace tinymce.html 30380 */ 30381 30382 /** 30383 * Contains the different UI types such as buttons, listboxes etc. 30384 * 30385 * @namespace tinymce.ui 30386 */ 30387 30388 /** 30389 * Contains various utility classes such as json parser, cookies etc. 30390 * 30391 * @namespace tinymce.util 30392 */ 30393 30394 // Included from: js/tinymce/classes/ui/Layout.js 30395 30396 /** 30397 * Layout.js 30398 * 30399 * Copyright, Moxiecode Systems AB 30400 * Released under LGPL License. 30401 * 30402 * License: http://www.tinymce.com/license 30403 * Contributing: http://www.tinymce.com/contributing 30404 */ 30405 30406 /** 30407 * Base layout manager class. 30408 * 30409 * @class tinymce.ui.Layout 30410 */ 30411 define("tinymce/ui/Layout", [ 30412 "tinymce/util/Class", 30413 "tinymce/util/Tools" 30414 ], function(Class, Tools) { 30415 "use strict"; 30416 30417 return Class.extend({ 30418 Defaults: { 30419 firstControlClass: 'first', 30420 lastControlClass: 'last' 30421 }, 30422 30423 /** 30424 * Constructs a layout instance with the specified settings. 30425 * 30426 * @constructor 30427 * @param {Object} settings Name/value object with settings. 30428 */ 30429 init: function(settings) { 30430 this.settings = Tools.extend({}, this.Defaults, settings); 30431 }, 30432 30433 /** 30434 * This method gets invoked before the layout renders the controls. 30435 * 30436 * @method preRender 30437 * @param {tinymce.ui.Container} container Container instance to preRender. 30438 */ 30439 preRender: function(container) { 30440 container.addClass(this.settings.containerClass, 'body'); 30441 }, 30442 30443 /** 30444 * Applies layout classes to the container. 30445 * 30446 * @private 30447 */ 30448 applyClasses: function(container) { 30449 var self = this, settings = self.settings, items, firstClass, lastClass; 30450 30451 items = container.items().filter(':visible'); 30452 firstClass = settings.firstControlClass; 30453 lastClass = settings.lastControlClass; 30454 30455 items.each(function(item) { 30456 item.removeClass(firstClass).removeClass(lastClass); 30457 30458 if (settings.controlClass) { 30459 item.addClass(settings.controlClass); 30460 } 30461 }); 30462 30463 items.eq(0).addClass(firstClass); 30464 items.eq(-1).addClass(lastClass); 30465 }, 30466 30467 /** 30468 * Renders the specified container and any layout specific HTML. 30469 * 30470 * @method renderHtml 30471 * @param {tinymce.ui.Container} container Container to render HTML for. 30472 */ 30473 renderHtml: function(container) { 30474 var self = this, settings = self.settings, items, html = ''; 30475 30476 items = container.items(); 30477 items.eq(0).addClass(settings.firstControlClass); 30478 items.eq(-1).addClass(settings.lastControlClass); 30479 30480 items.each(function(item) { 30481 if (settings.controlClass) { 30482 item.addClass(settings.controlClass); 30483 } 30484 30485 html += item.renderHtml(); 30486 }); 30487 30488 return html; 30489 }, 30490 30491 /** 30492 * Recalculates the positions of the controls in the specified container. 30493 * 30494 * @method recalc 30495 * @param {tinymce.ui.Container} container Container instance to recalc. 30496 */ 30497 recalc: function() { 30498 }, 30499 30500 /** 30501 * This method gets invoked after the layout renders the controls. 30502 * 30503 * @method postRender 30504 * @param {tinymce.ui.Container} container Container instance to postRender. 30505 */ 30506 postRender: function() { 30507 } 30508 }); 30509 }); 30510 30511 // Included from: js/tinymce/classes/ui/AbsoluteLayout.js 30512 30513 /** 30514 * AbsoluteLayout.js 30515 * 30516 * Copyright, Moxiecode Systems AB 30517 * Released under LGPL License. 30518 * 30519 * License: http://www.tinymce.com/license 30520 * Contributing: http://www.tinymce.com/contributing 30521 */ 30522 30523 /** 30524 * LayoutManager for absolute positioning. This layout manager is more of 30525 * a base class for other layouts but can be created and used directly. 30526 * 30527 * @-x-less AbsoluteLayout.less 30528 * @class tinymce.ui.AbsoluteLayout 30529 * @extends tinymce.ui.Layout 30530 */ 30531 define("tinymce/ui/AbsoluteLayout", [ 30532 "tinymce/ui/Layout" 30533 ], function(Layout) { 30534 "use strict"; 30535 30536 return Layout.extend({ 30537 Defaults: { 30538 containerClass: 'abs-layout', 30539 controlClass: 'abs-layout-item' 30540 }, 30541 30542 /** 30543 * Recalculates the positions of the controls in the specified container. 30544 * 30545 * @method recalc 30546 * @param {tinymce.ui.Container} container Container instance to recalc. 30547 */ 30548 recalc: function(container) { 30549 container.items().filter(':visible').each(function(ctrl) { 30550 var settings = ctrl.settings; 30551 30552 ctrl.layoutRect({ 30553 x: settings.x, 30554 y: settings.y, 30555 w: settings.w, 30556 h: settings.h 30557 }); 30558 30559 if (ctrl.recalc) { 30560 ctrl.recalc(); 30561 } 30562 }); 30563 }, 30564 30565 /** 30566 * Renders the specified container and any layout specific HTML. 30567 * 30568 * @method renderHtml 30569 * @param {tinymce.ui.Container} container Container to render HTML for. 30570 */ 30571 renderHtml: function(container) { 30572 return '<div id="' + container._id + '-absend" class="' + container.classPrefix + 'abs-end"></div>' + this._super(container); 30573 } 30574 }); 30575 }); 30576 30577 // Included from: js/tinymce/classes/ui/Tooltip.js 30578 30579 /** 30580 * Tooltip.js 30581 * 30582 * Copyright, Moxiecode Systems AB 30583 * Released under LGPL License. 30584 * 30585 * License: http://www.tinymce.com/license 30586 * Contributing: http://www.tinymce.com/contributing 30587 */ 30588 30589 /** 30590 * Creates a tooltip instance. 30591 * 30592 * @-x-less ToolTip.less 30593 * @class tinymce.ui.ToolTip 30594 * @extends tinymce.ui.Control 30595 * @mixes tinymce.ui.Movable 30596 */ 30597 define("tinymce/ui/Tooltip", [ 30598 "tinymce/ui/Control", 30599 "tinymce/ui/Movable" 30600 ], function(Control, Movable) { 30601 return Control.extend({ 30602 Mixins: [Movable], 30603 30604 Defaults: { 30605 classes: 'widget tooltip tooltip-n' 30606 }, 30607 30608 /** 30609 * Sets/gets the current label text. 30610 * 30611 * @method text 30612 * @param {String} [text] New label text. 30613 * @return {String|tinymce.ui.Tooltip} Current text or current label instance. 30614 */ 30615 text: function(value) { 30616 var self = this; 30617 30618 if (typeof(value) != "undefined") { 30619 self._value = value; 30620 30621 if (self._rendered) { 30622 self.getEl().lastChild.innerHTML = self.encode(value); 30623 } 30624 30625 return self; 30626 } 30627 30628 return self._value; 30629 }, 30630 30631 /** 30632 * Renders the control as a HTML string. 30633 * 30634 * @method renderHtml 30635 * @return {String} HTML representing the control. 30636 */ 30637 renderHtml: function() { 30638 var self = this, prefix = self.classPrefix; 30639 30640 return ( 30641 '<div id="' + self._id + '" class="' + self.classes() + '" role="presentation">' + 30642 '<div class="' + prefix + 'tooltip-arrow"></div>' + 30643 '<div class="' + prefix + 'tooltip-inner">' + self.encode(self._text) + '</div>' + 30644 '</div>' 30645 ); 30646 }, 30647 30648 /** 30649 * Repaints the control after a layout operation. 30650 * 30651 * @method repaint 30652 */ 30653 repaint: function() { 30654 var self = this, style, rect; 30655 30656 style = self.getEl().style; 30657 rect = self._layoutRect; 30658 30659 style.left = rect.x + 'px'; 30660 style.top = rect.y + 'px'; 30661 style.zIndex = 0xFFFF + 0xFFFF; 30662 } 30663 }); 30664 }); 30665 30666 // Included from: js/tinymce/classes/ui/Widget.js 30667 30668 /** 30669 * Widget.js 30670 * 30671 * Copyright, Moxiecode Systems AB 30672 * Released under LGPL License. 30673 * 30674 * License: http://www.tinymce.com/license 30675 * Contributing: http://www.tinymce.com/contributing 30676 */ 30677 30678 /** 30679 * Widget base class a widget is a control that has a tooltip and some basic states. 30680 * 30681 * @class tinymce.ui.Widget 30682 * @extends tinymce.ui.Control 30683 */ 30684 define("tinymce/ui/Widget", [ 30685 "tinymce/ui/Control", 30686 "tinymce/ui/Tooltip" 30687 ], function(Control, Tooltip) { 30688 "use strict"; 30689 30690 var tooltip; 30691 30692 var Widget = Control.extend({ 30693 /** 30694 * Constructs a instance with the specified settings. 30695 * 30696 * @constructor 30697 * @param {Object} settings Name/value object with settings. 30698 * @setting {String} tooltip Tooltip text to display when hovering. 30699 * @setting {Boolean} autofocus True if the control should be focused when rendered. 30700 * @setting {String} text Text to display inside widget. 30701 */ 30702 init: function(settings) { 30703 var self = this; 30704 30705 self._super(settings); 30706 settings = self.settings; 30707 self.canFocus = true; 30708 30709 if (settings.tooltip && Widget.tooltips !== false) { 30710 self.on('mouseenter', function(e) { 30711 var tooltip = self.tooltip().moveTo(-0xFFFF); 30712 30713 if (e.control == self) { 30714 var rel = tooltip.text(settings.tooltip).show().testMoveRel(self.getEl(), ['bc-tc', 'bc-tl', 'bc-tr']); 30715 30716 tooltip.toggleClass('tooltip-n', rel == 'bc-tc'); 30717 tooltip.toggleClass('tooltip-nw', rel == 'bc-tl'); 30718 tooltip.toggleClass('tooltip-ne', rel == 'bc-tr'); 30719 30720 tooltip.moveRel(self.getEl(), rel); 30721 } else { 30722 tooltip.hide(); 30723 } 30724 }); 30725 30726 self.on('mouseleave mousedown click', function() { 30727 self.tooltip().hide(); 30728 }); 30729 } 30730 30731 self.aria('label', settings.ariaLabel || settings.tooltip); 30732 }, 30733 30734 /** 30735 * Returns the current tooltip instance. 30736 * 30737 * @method tooltip 30738 * @return {tinymce.ui.Tooltip} Tooltip instance. 30739 */ 30740 tooltip: function() { 30741 if (!tooltip) { 30742 tooltip = new Tooltip({type: 'tooltip'}); 30743 tooltip.renderTo(); 30744 } 30745 30746 return tooltip; 30747 }, 30748 30749 /** 30750 * Sets/gets the active state of the widget. 30751 * 30752 * @method active 30753 * @param {Boolean} [state] State if the control is active. 30754 * @return {Boolean|tinymce.ui.Widget} True/false or current widget instance. 30755 */ 30756 active: function(state) { 30757 var self = this, undef; 30758 30759 if (state !== undef) { 30760 self.aria('pressed', state); 30761 self.toggleClass('active', state); 30762 } 30763 30764 return self._super(state); 30765 }, 30766 30767 /** 30768 * Sets/gets the disabled state of the widget. 30769 * 30770 * @method disabled 30771 * @param {Boolean} [state] State if the control is disabled. 30772 * @return {Boolean|tinymce.ui.Widget} True/false or current widget instance. 30773 */ 30774 disabled: function(state) { 30775 var self = this, undef; 30776 30777 if (state !== undef) { 30778 self.aria('disabled', state); 30779 self.toggleClass('disabled', state); 30780 } 30781 30782 return self._super(state); 30783 }, 30784 30785 /** 30786 * Called after the control has been rendered. 30787 * 30788 * @method postRender 30789 */ 30790 postRender: function() { 30791 var self = this, settings = self.settings; 30792 30793 self._rendered = true; 30794 30795 self._super(); 30796 30797 if (!self.parent() && (settings.width || settings.height)) { 30798 self.initLayoutRect(); 30799 self.repaint(); 30800 } 30801 30802 if (settings.autofocus) { 30803 self.focus(); 30804 } 30805 }, 30806 30807 /** 30808 * Removes the current control from DOM and from UI collections. 30809 * 30810 * @method remove 30811 * @return {tinymce.ui.Control} Current control instance. 30812 */ 30813 remove: function() { 30814 this._super(); 30815 30816 if (tooltip) { 30817 tooltip.remove(); 30818 tooltip = null; 30819 } 30820 } 30821 }); 30822 30823 return Widget; 30824 }); 30825 30826 // Included from: js/tinymce/classes/ui/Button.js 30827 30828 /** 30829 * Button.js 30830 * 30831 * Copyright, Moxiecode Systems AB 30832 * Released under LGPL License. 30833 * 30834 * License: http://www.tinymce.com/license 30835 * Contributing: http://www.tinymce.com/contributing 30836 */ 30837 30838 /** 30839 * This class is used to create buttons. You can create them directly or through the Factory. 30840 * 30841 * @example 30842 * // Create and render a button to the body element 30843 * tinymce.ui.Factory.create({ 30844 * type: 'button', 30845 * text: 'My button' 30846 * }).renderTo(document.body); 30847 * 30848 * @-x-less Button.less 30849 * @class tinymce.ui.Button 30850 * @extends tinymce.ui.Widget 30851 */ 30852 define("tinymce/ui/Button", [ 30853 "tinymce/ui/Widget" 30854 ], function(Widget) { 30855 "use strict"; 30856 30857 return Widget.extend({ 30858 Defaults: { 30859 classes: "widget btn", 30860 role: "button" 30861 }, 30862 30863 /** 30864 * Constructs a new button instance with the specified settings. 30865 * 30866 * @constructor 30867 * @param {Object} settings Name/value object with settings. 30868 * @setting {String} size Size of the button small|medium|large. 30869 * @setting {String} image Image to use for icon. 30870 * @setting {String} icon Icon to use for button. 30871 */ 30872 init: function(settings) { 30873 var self = this, size; 30874 30875 self.on('click mousedown', function(e) { 30876 e.preventDefault(); 30877 }); 30878 30879 self._super(settings); 30880 size = settings.size; 30881 30882 if (settings.subtype) { 30883 self.addClass(settings.subtype); 30884 } 30885 30886 if (size) { 30887 self.addClass('btn-' + size); 30888 } 30889 }, 30890 30891 /** 30892 * Sets/gets the current button icon. 30893 * 30894 * @method icon 30895 * @param {String} [icon] New icon identifier. 30896 * @return {String|tinymce.ui.MenuButton} Current icon or current MenuButton instance. 30897 */ 30898 icon: function(icon) { 30899 var self = this, prefix = self.classPrefix; 30900 30901 if (typeof(icon) == 'undefined') { 30902 return self.settings.icon; 30903 } 30904 30905 self.settings.icon = icon; 30906 icon = icon ? prefix + 'ico ' + prefix + 'i-' + self.settings.icon : ''; 30907 30908 if (self._rendered) { 30909 var btnElm = self.getEl().firstChild, iconElm = btnElm.getElementsByTagName('i')[0]; 30910 30911 if (icon) { 30912 if (!iconElm || iconElm != btnElm.firstChild) { 30913 iconElm = document.createElement('i'); 30914 btnElm.insertBefore(iconElm, btnElm.firstChild); 30915 } 30916 30917 iconElm.className = icon; 30918 } else if (iconElm) { 30919 btnElm.removeChild(iconElm); 30920 } 30921 30922 self.text(self._text); // Set text again to fix whitespace between icon + text 30923 } 30924 30925 return self; 30926 }, 30927 30928 /** 30929 * Repaints the button for example after it's been resizes by a layout engine. 30930 * 30931 * @method repaint 30932 */ 30933 repaint: function() { 30934 var btnStyle = this.getEl().firstChild.style; 30935 30936 btnStyle.width = btnStyle.height = "100%"; 30937 30938 this._super(); 30939 }, 30940 30941 /** 30942 * Sets/gets the current button text. 30943 * 30944 * @method text 30945 * @param {String} [text] New button text. 30946 * @return {String|tinymce.ui.Button} Current text or current Button instance. 30947 */ 30948 text: function(text) { 30949 var self = this; 30950 30951 if (self._rendered) { 30952 var textNode = self.getEl().lastChild.lastChild; 30953 if (textNode) { 30954 textNode.data = self.translate(text); 30955 } 30956 } 30957 30958 return self._super(text); 30959 }, 30960 30961 /** 30962 * Renders the control as a HTML string. 30963 * 30964 * @method renderHtml 30965 * @return {String} HTML representing the control. 30966 */ 30967 renderHtml: function() { 30968 var self = this, id = self._id, prefix = self.classPrefix; 30969 var icon = self.settings.icon, image; 30970 30971 image = self.settings.image; 30972 if (image) { 30973 icon = 'none'; 30974 30975 // Support for [high dpi, low dpi] image sources 30976 if (typeof image != "string") { 30977 image = window.getSelection ? image[0] : image[1]; 30978 } 30979 30980 image = ' style="background-image: url(\'' + image + '\')"'; 30981 } else { 30982 image = ''; 30983 } 30984 30985 icon = self.settings.icon ? prefix + 'ico ' + prefix + 'i-' + icon : ''; 30986 30987 return ( 30988 '<div id="' + id + '" class="' + self.classes() + '" tabindex="-1" aria-labelledby="' + id + '">' + 30989 '<button role="presentation" type="button" tabindex="-1">' + 30990 (icon ? '<i class="' + icon + '"' + image + '></i>' : '') + 30991 (self._text ? (icon ? '\u00a0' : '') + self.encode(self._text) : '') + 30992 '</button>' + 30993 '</div>' 30994 ); 30995 } 30996 }); 30997 }); 30998 30999 // Included from: js/tinymce/classes/ui/ButtonGroup.js 31000 31001 /** 31002 * ButtonGroup.js 31003 * 31004 * Copyright, Moxiecode Systems AB 31005 * Released under LGPL License. 31006 * 31007 * License: http://www.tinymce.com/license 31008 * Contributing: http://www.tinymce.com/contributing 31009 */ 31010 31011 /** 31012 * This control enables you to put multiple buttons into a group. This is 31013 * useful when you want to combine similar toolbar buttons into a group. 31014 * 31015 * @example 31016 * // Create and render a buttongroup with two buttons to the body element 31017 * tinymce.ui.Factory.create({ 31018 * type: 'buttongroup', 31019 * items: [ 31020 * {text: 'Button A'}, 31021 * {text: 'Button B'} 31022 * ] 31023 * }).renderTo(document.body); 31024 * 31025 * @-x-less ButtonGroup.less 31026 * @class tinymce.ui.ButtonGroup 31027 * @extends tinymce.ui.Container 31028 */ 31029 define("tinymce/ui/ButtonGroup", [ 31030 "tinymce/ui/Container" 31031 ], function(Container) { 31032 "use strict"; 31033 31034 return Container.extend({ 31035 Defaults: { 31036 defaultType: 'button', 31037 role: 'group' 31038 }, 31039 31040 /** 31041 * Renders the control as a HTML string. 31042 * 31043 * @method renderHtml 31044 * @return {String} HTML representing the control. 31045 */ 31046 renderHtml: function() { 31047 var self = this, layout = self._layout; 31048 31049 self.addClass('btn-group'); 31050 self.preRender(); 31051 layout.preRender(self); 31052 31053 return ( 31054 '<div id="' + self._id + '" class="' + self.classes() + '">' + 31055 '<div id="' + self._id + '-body">' + 31056 (self.settings.html || '') + layout.renderHtml(self) + 31057 '</div>' + 31058 '</div>' 31059 ); 31060 } 31061 }); 31062 }); 31063 31064 // Included from: js/tinymce/classes/ui/Checkbox.js 31065 31066 /** 31067 * Checkbox.js 31068 * 31069 * Copyright, Moxiecode Systems AB 31070 * Released under LGPL License. 31071 * 31072 * License: http://www.tinymce.com/license 31073 * Contributing: http://www.tinymce.com/contributing 31074 */ 31075 31076 /** 31077 * This control creates a custom checkbox. 31078 * 31079 * @example 31080 * // Create and render a checkbox to the body element 31081 * tinymce.ui.Factory.create({ 31082 * type: 'checkbox', 31083 * checked: true, 31084 * text: 'My checkbox' 31085 * }).renderTo(document.body); 31086 * 31087 * @-x-less Checkbox.less 31088 * @class tinymce.ui.Checkbox 31089 * @extends tinymce.ui.Widget 31090 */ 31091 define("tinymce/ui/Checkbox", [ 31092 "tinymce/ui/Widget" 31093 ], function(Widget) { 31094 "use strict"; 31095 31096 return Widget.extend({ 31097 Defaults: { 31098 classes: "checkbox", 31099 role: "checkbox", 31100 checked: false 31101 }, 31102 31103 /** 31104 * Constructs a new Checkbox instance with the specified settings. 31105 * 31106 * @constructor 31107 * @param {Object} settings Name/value object with settings. 31108 * @setting {Boolean} checked True if the checkbox should be checked by default. 31109 */ 31110 init: function(settings) { 31111 var self = this; 31112 31113 self._super(settings); 31114 31115 self.on('click mousedown', function(e) { 31116 e.preventDefault(); 31117 }); 31118 31119 self.on('click', function(e) { 31120 e.preventDefault(); 31121 31122 if (!self.disabled()) { 31123 self.checked(!self.checked()); 31124 } 31125 }); 31126 31127 self.checked(self.settings.checked); 31128 }, 31129 31130 /** 31131 * Getter/setter function for the checked state. 31132 * 31133 * @method checked 31134 * @param {Boolean} [state] State to be set. 31135 * @return {Boolean|tinymce.ui.Checkbox} True/false or checkbox if it's a set operation. 31136 */ 31137 checked: function(state) { 31138 var self = this; 31139 31140 if (typeof state != "undefined") { 31141 if (state) { 31142 self.addClass('checked'); 31143 } else { 31144 self.removeClass('checked'); 31145 } 31146 31147 self._checked = state; 31148 self.aria('checked', state); 31149 31150 return self; 31151 } 31152 31153 return self._checked; 31154 }, 31155 31156 /** 31157 * Getter/setter function for the value state. 31158 * 31159 * @method value 31160 * @param {Boolean} [state] State to be set. 31161 * @return {Boolean|tinymce.ui.Checkbox} True/false or checkbox if it's a set operation. 31162 */ 31163 value: function(state) { 31164 return this.checked(state); 31165 }, 31166 31167 /** 31168 * Renders the control as a HTML string. 31169 * 31170 * @method renderHtml 31171 * @return {String} HTML representing the control. 31172 */ 31173 renderHtml: function() { 31174 var self = this, id = self._id, prefix = self.classPrefix; 31175 31176 return ( 31177 '<div id="' + id + '" class="' + self.classes() + '" unselectable="on" aria-labelledby="' + id + '-al" tabindex="-1">' + 31178 '<i class="' + prefix + 'ico ' + prefix + 'i-checkbox"></i>' + 31179 '<span id="' + id + '-al" class="' + prefix + 'label">' + self.encode(self._text) + '</span>' + 31180 '</div>' 31181 ); 31182 } 31183 }); 31184 }); 31185 31186 // Included from: js/tinymce/classes/ui/PanelButton.js 31187 31188 /** 31189 * PanelButton.js 31190 * 31191 * Copyright, Moxiecode Systems AB 31192 * Released under LGPL License. 31193 * 31194 * License: http://www.tinymce.com/license 31195 * Contributing: http://www.tinymce.com/contributing 31196 */ 31197 31198 /** 31199 * Creates a new panel button. 31200 * 31201 * @class tinymce.ui.PanelButton 31202 * @extends tinymce.ui.Button 31203 */ 31204 define("tinymce/ui/PanelButton", [ 31205 "tinymce/ui/Button", 31206 "tinymce/ui/FloatPanel" 31207 ], function(Button, FloatPanel) { 31208 "use strict"; 31209 31210 return Button.extend({ 31211 /** 31212 * Shows the panel for the button. 31213 * 31214 * @method showPanel 31215 */ 31216 showPanel: function() { 31217 var self = this, settings = self.settings; 31218 31219 self.active(true); 31220 31221 if (!self.panel) { 31222 var panelSettings = settings.panel; 31223 31224 // Wrap panel in grid layout if type if specified 31225 // This makes it possible to add forms or other containers directly in the panel option 31226 if (panelSettings.type) { 31227 panelSettings = { 31228 layout: 'grid', 31229 items: panelSettings 31230 }; 31231 } 31232 31233 panelSettings.role = panelSettings.role || 'dialog'; 31234 panelSettings.popover = true; 31235 panelSettings.autohide = true; 31236 panelSettings.ariaRoot = true; 31237 31238 self.panel = new FloatPanel(panelSettings).on('hide', function() { 31239 self.active(false); 31240 }).on('cancel', function(e) { 31241 e.stopPropagation(); 31242 self.focus(); 31243 self.hidePanel(); 31244 }).parent(self).renderTo(self.getContainerElm()); 31245 31246 self.panel.fire('show'); 31247 self.panel.reflow(); 31248 } else { 31249 self.panel.show(); 31250 } 31251 31252 self.panel.moveRel(self.getEl(), settings.popoverAlign || (self.isRtl() ? ['bc-tr', 'bc-tc'] : ['bc-tl', 'bc-tc'])); 31253 }, 31254 31255 /** 31256 * Hides the panel for the button. 31257 * 31258 * @method hidePanel 31259 */ 31260 hidePanel: function() { 31261 var self = this; 31262 31263 if (self.panel) { 31264 self.panel.hide(); 31265 } 31266 }, 31267 31268 /** 31269 * Called after the control has been rendered. 31270 * 31271 * @method postRender 31272 */ 31273 postRender: function() { 31274 var self = this; 31275 31276 self.aria('haspopup', true); 31277 31278 self.on('click', function(e) { 31279 if (e.control === self) { 31280 if (self.panel && self.panel.visible()) { 31281 self.hidePanel(); 31282 } else { 31283 self.showPanel(); 31284 self.panel.focus(!!e.aria); 31285 } 31286 } 31287 }); 31288 31289 return self._super(); 31290 } 31291 }); 31292 }); 31293 31294 // Included from: js/tinymce/classes/ui/ColorButton.js 31295 31296 /** 31297 * ColorButton.js 31298 * 31299 * Copyright, Moxiecode Systems AB 31300 * Released under LGPL License. 31301 * 31302 * License: http://www.tinymce.com/license 31303 * Contributing: http://www.tinymce.com/contributing 31304 */ 31305 31306 /** 31307 * This class creates a color button control. This is a split button in which the main 31308 * button has a visual representation of the currently selected color. When clicked 31309 * the caret button displays a color picker, allowing the user to select a new color. 31310 * 31311 * @-x-less ColorButton.less 31312 * @class tinymce.ui.ColorButton 31313 * @extends tinymce.ui.PanelButton 31314 */ 31315 define("tinymce/ui/ColorButton", [ 31316 "tinymce/ui/PanelButton", 31317 "tinymce/dom/DOMUtils" 31318 ], function(PanelButton, DomUtils) { 31319 "use strict"; 31320 31321 var DOM = DomUtils.DOM; 31322 31323 return PanelButton.extend({ 31324 /** 31325 * Constructs a new ColorButton instance with the specified settings. 31326 * 31327 * @constructor 31328 * @param {Object} settings Name/value object with settings. 31329 */ 31330 init: function(settings) { 31331 this._super(settings); 31332 this.addClass('colorbutton'); 31333 }, 31334 31335 /** 31336 * Getter/setter for the current color. 31337 * 31338 * @method color 31339 * @param {String} [color] Color to set. 31340 * @return {String|tinymce.ui.ColorButton} Current color or current instance. 31341 */ 31342 color: function(color) { 31343 if (color) { 31344 this._color = color; 31345 this.getEl('preview').style.backgroundColor = color; 31346 return this; 31347 } 31348 31349 return this._color; 31350 }, 31351 31352 /** 31353 * Renders the control as a HTML string. 31354 * 31355 * @method renderHtml 31356 * @return {String} HTML representing the control. 31357 */ 31358 renderHtml: function() { 31359 var self = this, id = self._id, prefix = self.classPrefix; 31360 var icon = self.settings.icon ? prefix + 'ico ' + prefix + 'i-' + self.settings.icon : ''; 31361 var image = self.settings.image ? ' style="background-image: url(\'' + self.settings.image + '\')"' : ''; 31362 31363 return ( 31364 '<div id="' + id + '" class="' + self.classes() + '" role="button" tabindex="-1" aria-haspopup="true">' + 31365 '<button role="presentation" hidefocus="1" type="button" tabindex="-1">' + 31366 (icon ? '<i class="' + icon + '"' + image + '></i>' : '') + 31367 '<span id="' + id + '-preview" class="' + prefix + 'preview"></span>' + 31368 (self._text ? (icon ? ' ' : '') + (self._text) : '') + 31369 '</button>' + 31370 '<button type="button" class="' + prefix + 'open" hidefocus="1" tabindex="-1">' + 31371 ' <i class="' + prefix + 'caret"></i>' + 31372 '</button>' + 31373 '</div>' 31374 ); 31375 }, 31376 31377 /** 31378 * Called after the control has been rendered. 31379 * 31380 * @method postRender 31381 */ 31382 postRender: function() { 31383 var self = this, onClickHandler = self.settings.onclick; 31384 31385 self.on('click', function(e) { 31386 if (e.aria && e.aria.key == 'down') { 31387 return; 31388 } 31389 31390 if (e.control == self && !DOM.getParent(e.target, '.' + self.classPrefix + 'open')) { 31391 e.stopImmediatePropagation(); 31392 onClickHandler.call(self, e); 31393 } 31394 }); 31395 31396 delete self.settings.onclick; 31397 31398 return self._super(); 31399 } 31400 31401 }); 31402 }); 31403 31404 // Included from: js/tinymce/classes/ui/ComboBox.js 31405 31406 /** 31407 * ComboBox.js 31408 * 31409 * Copyright, Moxiecode Systems AB 31410 * Released under LGPL License. 31411 * 31412 * License: http://www.tinymce.com/license 31413 * Contributing: http://www.tinymce.com/contributing 31414 */ 31415 31416 /** 31417 * This class creates a combobox control. Select box that you select a value from or 31418 * type a value into. 31419 * 31420 * @-x-less ComboBox.less 31421 * @class tinymce.ui.ComboBox 31422 * @extends tinymce.ui.Widget 31423 */ 31424 define("tinymce/ui/ComboBox", [ 31425 "tinymce/ui/Widget", 31426 "tinymce/ui/Factory", 31427 "tinymce/ui/DomUtils" 31428 ], function(Widget, Factory, DomUtils) { 31429 "use strict"; 31430 31431 return Widget.extend({ 31432 /** 31433 * Constructs a new control instance with the specified settings. 31434 * 31435 * @constructor 31436 * @param {Object} settings Name/value object with settings. 31437 * @setting {String} placeholder Placeholder text to display. 31438 */ 31439 init: function(settings) { 31440 var self = this; 31441 31442 self._super(settings); 31443 self.addClass('combobox'); 31444 self.subinput = true; 31445 self.ariaTarget = 'inp'; // TODO: Figure out a better way 31446 31447 settings = self.settings; 31448 settings.menu = settings.menu || settings.values; 31449 31450 if (settings.menu) { 31451 settings.icon = 'caret'; 31452 } 31453 31454 self.on('click', function(e) { 31455 var elm = e.target, root = self.getEl(); 31456 31457 while (elm && elm != root) { 31458 if (elm.id && elm.id.indexOf('-open') != -1) { 31459 self.fire('action'); 31460 31461 if (settings.menu) { 31462 self.showMenu(); 31463 31464 if (e.aria) { 31465 self.menu.items()[0].focus(); 31466 } 31467 } 31468 } 31469 31470 elm = elm.parentNode; 31471 } 31472 }); 31473 31474 // TODO: Rework this 31475 self.on('keydown', function(e) { 31476 if (e.target.nodeName == "INPUT" && e.keyCode == 13) { 31477 self.parents().reverse().each(function(ctrl) { 31478 e.preventDefault(); 31479 self.fire('change'); 31480 31481 if (ctrl.hasEventListeners('submit') && ctrl.toJSON) { 31482 ctrl.fire('submit', {data: ctrl.toJSON()}); 31483 return false; 31484 } 31485 }); 31486 } 31487 }); 31488 31489 if (settings.placeholder) { 31490 self.addClass('placeholder'); 31491 31492 self.on('focusin', function() { 31493 if (!self._hasOnChange) { 31494 DomUtils.on(self.getEl('inp'), 'change', function() { 31495 self.fire('change'); 31496 }); 31497 31498 self._hasOnChange = true; 31499 } 31500 31501 if (self.hasClass('placeholder')) { 31502 self.getEl('inp').value = ''; 31503 self.removeClass('placeholder'); 31504 } 31505 }); 31506 31507 self.on('focusout', function() { 31508 if (self.value().length === 0) { 31509 self.getEl('inp').value = settings.placeholder; 31510 self.addClass('placeholder'); 31511 } 31512 }); 31513 } 31514 }, 31515 31516 showMenu: function() { 31517 var self = this, settings = self.settings, menu; 31518 31519 if (!self.menu) { 31520 menu = settings.menu || []; 31521 31522 // Is menu array then auto constuct menu control 31523 if (menu.length) { 31524 menu = { 31525 type: 'menu', 31526 items: menu 31527 }; 31528 } else { 31529 menu.type = menu.type || 'menu'; 31530 } 31531 31532 self.menu = Factory.create(menu).parent(self).renderTo(self.getContainerElm()); 31533 self.fire('createmenu'); 31534 self.menu.reflow(); 31535 self.menu.on('cancel', function(e) { 31536 if (e.control === self.menu) { 31537 self.focus(); 31538 } 31539 }); 31540 31541 self.menu.on('show hide', function(e) { 31542 e.control.items().each(function(ctrl) { 31543 ctrl.active(ctrl.value() == self.value()); 31544 }); 31545 }).fire('show'); 31546 31547 self.menu.on('select', function(e) { 31548 self.value(e.control.value()); 31549 }); 31550 31551 self.on('focusin', function(e) { 31552 if (e.target.tagName.toUpperCase() == 'INPUT') { 31553 self.menu.hide(); 31554 } 31555 }); 31556 31557 self.aria('expanded', true); 31558 } 31559 31560 self.menu.show(); 31561 self.menu.layoutRect({w: self.layoutRect().w}); 31562 self.menu.moveRel(self.getEl(), self.isRtl() ? ['br-tr', 'tr-br'] : ['bl-tl', 'tl-bl']); 31563 }, 31564 31565 /** 31566 * Getter/setter function for the control value. 31567 * 31568 * @method value 31569 * @param {String} [value] Value to be set. 31570 * @return {String|tinymce.ui.ComboBox} Value or self if it's a set operation. 31571 */ 31572 value: function(value) { 31573 var self = this; 31574 31575 if (typeof(value) != "undefined") { 31576 self._value = value; 31577 self.removeClass('placeholder'); 31578 31579 if (self._rendered) { 31580 self.getEl('inp').value = value; 31581 } 31582 31583 return self; 31584 } 31585 31586 if (self._rendered) { 31587 value = self.getEl('inp').value; 31588 31589 if (value != self.settings.placeholder) { 31590 return value; 31591 } 31592 31593 return ''; 31594 } 31595 31596 return self._value; 31597 }, 31598 31599 /** 31600 * Getter/setter function for the disabled state. 31601 * 31602 * @method value 31603 * @param {Boolean} [state] State to be set. 31604 * @return {Boolean|tinymce.ui.ComboBox} True/false or self if it's a set operation. 31605 */ 31606 disabled: function(state) { 31607 var self = this; 31608 31609 if (self._rendered && typeof(state) != 'undefined') { 31610 self.getEl('inp').disabled = state; 31611 } 31612 31613 return self._super(state); 31614 }, 31615 31616 /** 31617 * Focuses the input area of the control. 31618 * 31619 * @method focus 31620 */ 31621 focus: function() { 31622 this.getEl('inp').focus(); 31623 }, 31624 31625 /** 31626 * Repaints the control after a layout operation. 31627 * 31628 * @method repaint 31629 */ 31630 repaint: function() { 31631 var self = this, elm = self.getEl(), openElm = self.getEl('open'), rect = self.layoutRect(); 31632 var width, lineHeight; 31633 31634 if (openElm) { 31635 width = rect.w - DomUtils.getSize(openElm).width - 10; 31636 } else { 31637 width = rect.w - 10; 31638 } 31639 31640 // Detect old IE 7+8 add lineHeight to align caret vertically in the middle 31641 var doc = document; 31642 if (doc.all && (!doc.documentMode || doc.documentMode <= 8)) { 31643 lineHeight = (self.layoutRect().h - 2) + 'px'; 31644 } 31645 31646 DomUtils.css(elm.firstChild, { 31647 width: width, 31648 lineHeight: lineHeight 31649 }); 31650 31651 self._super(); 31652 31653 return self; 31654 }, 31655 31656 /** 31657 * Post render method. Called after the control has been rendered to the target. 31658 * 31659 * @method postRender 31660 * @return {tinymce.ui.ComboBox} Current combobox instance. 31661 */ 31662 postRender: function() { 31663 var self = this; 31664 31665 DomUtils.on(this.getEl('inp'), 'change', function() { 31666 self.fire('change'); 31667 }); 31668 31669 return self._super(); 31670 }, 31671 31672 remove: function() { 31673 DomUtils.off(this.getEl('inp')); 31674 this._super(); 31675 }, 31676 31677 /** 31678 * Renders the control as a HTML string. 31679 * 31680 * @method renderHtml 31681 * @return {String} HTML representing the control. 31682 */ 31683 renderHtml: function() { 31684 var self = this, id = self._id, settings = self.settings, prefix = self.classPrefix; 31685 var value = settings.value || settings.placeholder || ''; 31686 var icon, text, openBtnHtml = '', extraAttrs = ''; 31687 31688 if ("spellcheck" in settings) { 31689 extraAttrs += ' spellcheck="' + settings.spellcheck + '"'; 31690 } 31691 31692 if (settings.maxLength) { 31693 extraAttrs += ' maxlength="' + settings.maxLength + '"'; 31694 } 31695 31696 if (settings.size) { 31697 extraAttrs += ' size="' + settings.size + '"'; 31698 } 31699 31700 if (settings.subtype) { 31701 extraAttrs += ' type="' + settings.subtype + '"'; 31702 } 31703 31704 if (self.disabled()) { 31705 extraAttrs += ' disabled="disabled"'; 31706 } 31707 31708 icon = settings.icon; 31709 if (icon && icon != 'caret') { 31710 icon = prefix + 'ico ' + prefix + 'i-' + settings.icon; 31711 } 31712 31713 text = self._text; 31714 31715 if (icon || text) { 31716 openBtnHtml = ( 31717 '<div id="' + id + '-open" class="' + prefix + 'btn ' + prefix + 'open" tabIndex="-1" role="button">' + 31718 '<button id="' + id + '-action" type="button" hidefocus="1" tabindex="-1">' + 31719 (icon != 'caret' ? '<i class="' + icon + '"></i>' : '<i class="' + prefix + 'caret"></i>') + 31720 (text ? (icon ? ' ' : '') + text : '') + 31721 '</button>' + 31722 '</div>' 31723 ); 31724 31725 self.addClass('has-open'); 31726 } 31727 31728 return ( 31729 '<div id="' + id + '" class="' + self.classes() + '">' + 31730 '<input id="' + id + '-inp" class="' + prefix + 'textbox ' + prefix + 'placeholder" value="' + 31731 value + '" hidefocus="1"' + extraAttrs + ' />' + 31732 openBtnHtml + 31733 '</div>' 31734 ); 31735 } 31736 }); 31737 }); 31738 31739 // Included from: js/tinymce/classes/ui/Path.js 31740 31741 /** 31742 * Path.js 31743 * 31744 * Copyright, Moxiecode Systems AB 31745 * Released under LGPL License. 31746 * 31747 * License: http://www.tinymce.com/license 31748 * Contributing: http://www.tinymce.com/contributing 31749 */ 31750 31751 /** 31752 * Creates a new path control. 31753 * 31754 * @-x-less Path.less 31755 * @class tinymce.ui.Path 31756 * @extends tinymce.ui.Widget 31757 */ 31758 define("tinymce/ui/Path", [ 31759 "tinymce/ui/Widget" 31760 ], function(Widget) { 31761 "use strict"; 31762 31763 return Widget.extend({ 31764 /** 31765 * Constructs a instance with the specified settings. 31766 * 31767 * @constructor 31768 * @param {Object} settings Name/value object with settings. 31769 * @setting {String} delimiter Delimiter to display between items in path. 31770 */ 31771 init: function(settings) { 31772 var self = this; 31773 31774 if (!settings.delimiter) { 31775 settings.delimiter = '\u00BB'; 31776 } 31777 31778 self._super(settings); 31779 self.addClass('path'); 31780 self.canFocus = true; 31781 31782 self.on('click', function(e) { 31783 var index, target = e.target; 31784 31785 if ((index = target.getAttribute('data-index'))) { 31786 self.fire('select', {value: self.data()[index], index: index}); 31787 } 31788 }); 31789 }, 31790 31791 /** 31792 * Focuses the current control. 31793 * 31794 * @method focus 31795 * @return {tinymce.ui.Control} Current control instance. 31796 */ 31797 focus: function() { 31798 var self = this; 31799 31800 self.getEl().firstChild.focus(); 31801 31802 return self; 31803 }, 31804 31805 /** 31806 * Sets/gets the data to be used for the path. 31807 * 31808 * @method data 31809 * @param {Array} data Array with items name is rendered to path. 31810 */ 31811 data: function(data) { 31812 var self = this; 31813 31814 if (typeof(data) !== "undefined") { 31815 self._data = data; 31816 self.update(); 31817 31818 return self; 31819 } 31820 31821 return self._data; 31822 }, 31823 31824 /** 31825 * Updated the path. 31826 * 31827 * @private 31828 */ 31829 update: function() { 31830 this.innerHtml(this._getPathHtml()); 31831 }, 31832 31833 /** 31834 * Called after the control has been rendered. 31835 * 31836 * @method postRender 31837 */ 31838 postRender: function() { 31839 var self = this; 31840 31841 self._super(); 31842 31843 self.data(self.settings.data); 31844 }, 31845 31846 /** 31847 * Renders the control as a HTML string. 31848 * 31849 * @method renderHtml 31850 * @return {String} HTML representing the control. 31851 */ 31852 renderHtml: function() { 31853 var self = this; 31854 31855 return ( 31856 '<div id="' + self._id + '" class="' + self.classes() + '">' + 31857 self._getPathHtml() + 31858 '</div>' 31859 ); 31860 }, 31861 31862 _getPathHtml: function() { 31863 var self = this, parts = self._data || [], i, l, html = '', prefix = self.classPrefix; 31864 31865 for (i = 0, l = parts.length; i < l; i++) { 31866 html += ( 31867 (i > 0 ? '<div class="' + prefix + 'divider" aria-hidden="true"> ' + self.settings.delimiter + ' </div>' : '') + 31868 '<div role="button" class="' + prefix + 'path-item' + (i == l - 1 ? ' ' + prefix + 'last' : '') + '" data-index="' + 31869 i + '" tabindex="-1" id="' + self._id + '-' + i + '" aria-level="' + i + '">' + parts[i].name + '</div>' 31870 ); 31871 } 31872 31873 if (!html) { 31874 html = '<div class="' + prefix + 'path-item">\u00a0</div>'; 31875 } 31876 31877 return html; 31878 } 31879 }); 31880 }); 31881 31882 // Included from: js/tinymce/classes/ui/ElementPath.js 31883 31884 /** 31885 * ElementPath.js 31886 * 31887 * Copyright, Moxiecode Systems AB 31888 * Released under LGPL License. 31889 * 31890 * License: http://www.tinymce.com/license 31891 * Contributing: http://www.tinymce.com/contributing 31892 */ 31893 31894 /** 31895 * This control creates an path for the current selections parent elements in TinyMCE. 31896 * 31897 * @class tinymce.ui.ElementPath 31898 * @extends tinymce.ui.Path 31899 */ 31900 define("tinymce/ui/ElementPath", [ 31901 "tinymce/ui/Path", 31902 "tinymce/EditorManager" 31903 ], function(Path, EditorManager) { 31904 return Path.extend({ 31905 /** 31906 * Post render method. Called after the control has been rendered to the target. 31907 * 31908 * @method postRender 31909 * @return {tinymce.ui.ElementPath} Current combobox instance. 31910 */ 31911 postRender: function() { 31912 var self = this, editor = EditorManager.activeEditor; 31913 31914 function isHidden(elm) { 31915 if (elm.nodeType === 1) { 31916 if (elm.nodeName == "BR" || !!elm.getAttribute('data-mce-bogus')) { 31917 return true; 31918 } 31919 31920 if (elm.getAttribute('data-mce-type') === 'bookmark') { 31921 return true; 31922 } 31923 } 31924 31925 return false; 31926 } 31927 31928 self.on('select', function(e) { 31929 var parents = [], node, body = editor.getBody(); 31930 31931 editor.focus(); 31932 31933 node = editor.selection.getStart(); 31934 while (node && node != body) { 31935 if (!isHidden(node)) { 31936 parents.push(node); 31937 } 31938 31939 node = node.parentNode; 31940 } 31941 31942 editor.selection.select(parents[parents.length - 1 - e.index]); 31943 editor.nodeChanged(); 31944 }); 31945 31946 editor.on('nodeChange', function(e) { 31947 var parents = [], selectionParents = e.parents, i = selectionParents.length; 31948 31949 while (i--) { 31950 if (selectionParents[i].nodeType == 1 && !isHidden(selectionParents[i])) { 31951 var args = editor.fire('ResolveName', { 31952 name: selectionParents[i].nodeName.toLowerCase(), 31953 target: selectionParents[i] 31954 }); 31955 31956 parents.push({name: args.name}); 31957 } 31958 } 31959 31960 self.data(parents); 31961 }); 31962 31963 return self._super(); 31964 } 31965 }); 31966 }); 31967 31968 // Included from: js/tinymce/classes/ui/FormItem.js 31969 31970 /** 31971 * FormItem.js 31972 * 31973 * Copyright, Moxiecode Systems AB 31974 * Released under LGPL License. 31975 * 31976 * License: http://www.tinymce.com/license 31977 * Contributing: http://www.tinymce.com/contributing 31978 */ 31979 31980 /** 31981 * This class is a container created by the form element with 31982 * a label and control item. 31983 * 31984 * @class tinymce.ui.FormItem 31985 * @extends tinymce.ui.Container 31986 * @setting {String} label Label to display for the form item. 31987 */ 31988 define("tinymce/ui/FormItem", [ 31989 "tinymce/ui/Container" 31990 ], function(Container) { 31991 "use strict"; 31992 31993 return Container.extend({ 31994 Defaults: { 31995 layout: 'flex', 31996 align: 'center', 31997 defaults: { 31998 flex: 1 31999 } 32000 }, 32001 32002 /** 32003 * Renders the control as a HTML string. 32004 * 32005 * @method renderHtml 32006 * @return {String} HTML representing the control. 32007 */ 32008 renderHtml: function() { 32009 var self = this, layout = self._layout, prefix = self.classPrefix; 32010 32011 self.addClass('formitem'); 32012 layout.preRender(self); 32013 32014 return ( 32015 '<div id="' + self._id + '" class="' + self.classes() + '" hidefocus="1" tabindex="-1">' + 32016 (self.settings.title ? ('<div id="' + self._id + '-title" class="' + prefix + 'title">' + 32017 self.settings.title + '</div>') : '') + 32018 '<div id="' + self._id + '-body" class="' + self.classes('body') + '">' + 32019 (self.settings.html || '') + layout.renderHtml(self) + 32020 '</div>' + 32021 '</div>' 32022 ); 32023 } 32024 }); 32025 }); 32026 32027 // Included from: js/tinymce/classes/ui/Form.js 32028 32029 /** 32030 * Form.js 32031 * 32032 * Copyright, Moxiecode Systems AB 32033 * Released under LGPL License. 32034 * 32035 * License: http://www.tinymce.com/license 32036 * Contributing: http://www.tinymce.com/contributing 32037 */ 32038 32039 /** 32040 * This class creates a form container. A form container has the ability 32041 * to automatically wrap items in tinymce.ui.FormItem instances. 32042 * 32043 * Each FormItem instance is a container for the label and the item. 32044 * 32045 * @example 32046 * tinymce.ui.Factory.create({ 32047 * type: 'form', 32048 * items: [ 32049 * {type: 'textbox', label: 'My text box'} 32050 * ] 32051 * }).renderTo(document.body); 32052 * 32053 * @class tinymce.ui.Form 32054 * @extends tinymce.ui.Container 32055 */ 32056 define("tinymce/ui/Form", [ 32057 "tinymce/ui/Container", 32058 "tinymce/ui/FormItem" 32059 ], function(Container, FormItem) { 32060 "use strict"; 32061 32062 return Container.extend({ 32063 Defaults: { 32064 containerCls: 'form', 32065 layout: 'flex', 32066 direction: 'column', 32067 align: 'stretch', 32068 flex: 1, 32069 padding: 20, 32070 labelGap: 30, 32071 spacing: 10, 32072 callbacks: { 32073 submit: function() { 32074 this.submit(); 32075 } 32076 } 32077 }, 32078 32079 /** 32080 * This method gets invoked before the control is rendered. 32081 * 32082 * @method preRender 32083 */ 32084 preRender: function() { 32085 var self = this, items = self.items(); 32086 32087 // Wrap any labeled items in FormItems 32088 items.each(function(ctrl) { 32089 var formItem, label = ctrl.settings.label; 32090 32091 if (label) { 32092 formItem = new FormItem({ 32093 layout: 'flex', 32094 autoResize: "overflow", 32095 defaults: {flex: 1}, 32096 items: [ 32097 {type: 'label', id: ctrl._id + '-l', text: label, flex: 0, forId: ctrl._id, disabled: ctrl.disabled()} 32098 ] 32099 }); 32100 32101 formItem.type = 'formitem'; 32102 ctrl.aria('labelledby', ctrl._id + '-l'); 32103 32104 if (typeof(ctrl.settings.flex) == "undefined") { 32105 ctrl.settings.flex = 1; 32106 } 32107 32108 self.replace(ctrl, formItem); 32109 formItem.add(ctrl); 32110 } 32111 }); 32112 }, 32113 32114 /** 32115 * Recalcs label widths. 32116 * 32117 * @private 32118 */ 32119 recalcLabels: function() { 32120 var self = this, maxLabelWidth = 0, labels = [], i, labelGap; 32121 32122 if (self.settings.labelGapCalc === false) { 32123 return; 32124 } 32125 32126 self.items().filter('formitem').each(function(item) { 32127 var labelCtrl = item.items()[0], labelWidth = labelCtrl.getEl().clientWidth; 32128 32129 maxLabelWidth = labelWidth > maxLabelWidth ? labelWidth : maxLabelWidth; 32130 labels.push(labelCtrl); 32131 }); 32132 32133 labelGap = self.settings.labelGap || 0; 32134 32135 i = labels.length; 32136 while (i--) { 32137 labels[i].settings.minWidth = maxLabelWidth + labelGap; 32138 } 32139 }, 32140 32141 /** 32142 * Getter/setter for the visibility state. 32143 * 32144 * @method visible 32145 * @param {Boolean} [state] True/false state to show/hide. 32146 * @return {tinymce.ui.Form|Boolean} True/false state or current control. 32147 */ 32148 visible: function(state) { 32149 var val = this._super(state); 32150 32151 if (state === true && this._rendered) { 32152 this.recalcLabels(); 32153 } 32154 32155 return val; 32156 }, 32157 32158 /** 32159 * Fires a submit event with the serialized form. 32160 * 32161 * @method submit 32162 * @return {Object} Event arguments object. 32163 */ 32164 submit: function() { 32165 return this.fire('submit', {data: this.toJSON()}); 32166 }, 32167 32168 /** 32169 * Post render method. Called after the control has been rendered to the target. 32170 * 32171 * @method postRender 32172 * @return {tinymce.ui.ComboBox} Current combobox instance. 32173 */ 32174 postRender: function() { 32175 var self = this; 32176 32177 self._super(); 32178 self.recalcLabels(); 32179 self.fromJSON(self.settings.data); 32180 } 32181 }); 32182 }); 32183 32184 // Included from: js/tinymce/classes/ui/FieldSet.js 32185 32186 /** 32187 * FieldSet.js 32188 * 32189 * Copyright, Moxiecode Systems AB 32190 * Released under LGPL License. 32191 * 32192 * License: http://www.tinymce.com/license 32193 * Contributing: http://www.tinymce.com/contributing 32194 */ 32195 32196 /** 32197 * This class creates fieldset containers. 32198 * 32199 * @-x-less FieldSet.less 32200 * @class tinymce.ui.FieldSet 32201 * @extends tinymce.ui.Form 32202 */ 32203 define("tinymce/ui/FieldSet", [ 32204 "tinymce/ui/Form" 32205 ], function(Form) { 32206 "use strict"; 32207 32208 return Form.extend({ 32209 Defaults: { 32210 containerCls: 'fieldset', 32211 layout: 'flex', 32212 direction: 'column', 32213 align: 'stretch', 32214 flex: 1, 32215 padding: "25 15 5 15", 32216 labelGap: 30, 32217 spacing: 10, 32218 border: 1 32219 }, 32220 32221 /** 32222 * Renders the control as a HTML string. 32223 * 32224 * @method renderHtml 32225 * @return {String} HTML representing the control. 32226 */ 32227 renderHtml: function() { 32228 var self = this, layout = self._layout, prefix = self.classPrefix; 32229 32230 self.preRender(); 32231 layout.preRender(self); 32232 32233 return ( 32234 '<fieldset id="' + self._id + '" class="' + self.classes() + '" hidefocus="1" tabindex="-1">' + 32235 (self.settings.title ? ('<legend id="' + self._id + '-title" class="' + prefix + 'fieldset-title">' + 32236 self.settings.title + '</legend>') : '') + 32237 '<div id="' + self._id + '-body" class="' + self.classes('body') + '">' + 32238 (self.settings.html || '') + layout.renderHtml(self) + 32239 '</div>' + 32240 '</fieldset>' 32241 ); 32242 } 32243 }); 32244 }); 32245 32246 // Included from: js/tinymce/classes/ui/FilePicker.js 32247 32248 /** 32249 * FilePicker.js 32250 * 32251 * Copyright, Moxiecode Systems AB 32252 * Released under LGPL License. 32253 * 32254 * License: http://www.tinymce.com/license 32255 * Contributing: http://www.tinymce.com/contributing 32256 */ 32257 32258 /*global tinymce:true */ 32259 32260 /** 32261 * This class creates a file picker control. 32262 * 32263 * @class tinymce.ui.FilePicker 32264 * @extends tinymce.ui.ComboBox 32265 */ 32266 define("tinymce/ui/FilePicker", [ 32267 "tinymce/ui/ComboBox" 32268 ], function(ComboBox) { 32269 "use strict"; 32270 32271 return ComboBox.extend({ 32272 /** 32273 * Constructs a new control instance with the specified settings. 32274 * 32275 * @constructor 32276 * @param {Object} settings Name/value object with settings. 32277 */ 32278 init: function(settings) { 32279 var self = this, editor = tinymce.activeEditor, fileBrowserCallback; 32280 32281 settings.spellcheck = false; 32282 32283 fileBrowserCallback = editor.settings.file_browser_callback; 32284 if (fileBrowserCallback) { 32285 settings.icon = 'browse'; 32286 32287 settings.onaction = function() { 32288 fileBrowserCallback( 32289 self.getEl('inp').id, 32290 self.getEl('inp').value, 32291 settings.filetype, 32292 window 32293 ); 32294 }; 32295 } 32296 32297 self._super(settings); 32298 } 32299 }); 32300 }); 32301 32302 // Included from: js/tinymce/classes/ui/FitLayout.js 32303 32304 /** 32305 * FitLayout.js 32306 * 32307 * Copyright, Moxiecode Systems AB 32308 * Released under LGPL License. 32309 * 32310 * License: http://www.tinymce.com/license 32311 * Contributing: http://www.tinymce.com/contributing 32312 */ 32313 32314 /** 32315 * This layout manager will resize the control to be the size of it's parent container. 32316 * In other words width: 100% and height: 100%. 32317 * 32318 * @-x-less FitLayout.less 32319 * @class tinymce.ui.FitLayout 32320 * @extends tinymce.ui.AbsoluteLayout 32321 */ 32322 define("tinymce/ui/FitLayout", [ 32323 "tinymce/ui/AbsoluteLayout" 32324 ], function(AbsoluteLayout) { 32325 "use strict"; 32326 32327 return AbsoluteLayout.extend({ 32328 /** 32329 * Recalculates the positions of the controls in the specified container. 32330 * 32331 * @method recalc 32332 * @param {tinymce.ui.Container} container Container instance to recalc. 32333 */ 32334 recalc: function(container) { 32335 var contLayoutRect = container.layoutRect(), paddingBox = container.paddingBox(); 32336 32337 container.items().filter(':visible').each(function(ctrl) { 32338 ctrl.layoutRect({ 32339 x: paddingBox.left, 32340 y: paddingBox.top, 32341 w: contLayoutRect.innerW - paddingBox.right - paddingBox.left, 32342 h: contLayoutRect.innerH - paddingBox.top - paddingBox.bottom 32343 }); 32344 32345 if (ctrl.recalc) { 32346 ctrl.recalc(); 32347 } 32348 }); 32349 } 32350 }); 32351 }); 32352 32353 // Included from: js/tinymce/classes/ui/FlexLayout.js 32354 32355 /** 32356 * FlexLayout.js 32357 * 32358 * Copyright, Moxiecode Systems AB 32359 * Released under LGPL License. 32360 * 32361 * License: http://www.tinymce.com/license 32362 * Contributing: http://www.tinymce.com/contributing 32363 */ 32364 32365 /** 32366 * This layout manager works similar to the CSS flex box. 32367 * 32368 * @setting {String} direction row|row-reverse|column|column-reverse 32369 * @setting {Number} flex A positive-number to flex by. 32370 * @setting {String} align start|end|center|stretch 32371 * @setting {String} pack start|end|justify 32372 * 32373 * @class tinymce.ui.FlexLayout 32374 * @extends tinymce.ui.AbsoluteLayout 32375 */ 32376 define("tinymce/ui/FlexLayout", [ 32377 "tinymce/ui/AbsoluteLayout" 32378 ], function(AbsoluteLayout) { 32379 "use strict"; 32380 32381 return AbsoluteLayout.extend({ 32382 /** 32383 * Recalculates the positions of the controls in the specified container. 32384 * 32385 * @method recalc 32386 * @param {tinymce.ui.Container} container Container instance to recalc. 32387 */ 32388 recalc: function(container) { 32389 // A ton of variables, needs to be in the same scope for performance 32390 var i, l, items, contLayoutRect, contPaddingBox, contSettings, align, pack, spacing, totalFlex, availableSpace, direction; 32391 var ctrl, ctrlLayoutRect, ctrlSettings, flex, maxSizeItems = [], size, maxSize, ratio, rect, pos, maxAlignEndPos; 32392 var sizeName, minSizeName, posName, maxSizeName, beforeName, innerSizeName, deltaSizeName, contentSizeName; 32393 var alignAxisName, alignInnerSizeName, alignSizeName, alignMinSizeName, alignBeforeName, alignAfterName; 32394 var alignDeltaSizeName, alignContentSizeName; 32395 var max = Math.max, min = Math.min; 32396 32397 // Get container items, properties and settings 32398 items = container.items().filter(':visible'); 32399 contLayoutRect = container.layoutRect(); 32400 contPaddingBox = container._paddingBox; 32401 contSettings = container.settings; 32402 direction = container.isRtl() ? (contSettings.direction || 'row-reversed') : contSettings.direction; 32403 align = contSettings.align; 32404 pack = container.isRtl() ? (contSettings.pack || 'end') : contSettings.pack; 32405 spacing = contSettings.spacing || 0; 32406 32407 if (direction == "row-reversed" || direction == "column-reverse") { 32408 items = items.set(items.toArray().reverse()); 32409 direction = direction.split('-')[0]; 32410 } 32411 32412 // Setup axis variable name for row/column direction since the calculations is the same 32413 if (direction == "column") { 32414 posName = "y"; 32415 sizeName = "h"; 32416 minSizeName = "minH"; 32417 maxSizeName = "maxH"; 32418 innerSizeName = "innerH"; 32419 beforeName = 'top'; 32420 deltaSizeName = "deltaH"; 32421 contentSizeName = "contentH"; 32422 32423 alignBeforeName = "left"; 32424 alignSizeName = "w"; 32425 alignAxisName = "x"; 32426 alignInnerSizeName = "innerW"; 32427 alignMinSizeName = "minW"; 32428 alignAfterName = "right"; 32429 alignDeltaSizeName = "deltaW"; 32430 alignContentSizeName = "contentW"; 32431 } else { 32432 posName = "x"; 32433 sizeName = "w"; 32434 minSizeName = "minW"; 32435 maxSizeName = "maxW"; 32436 innerSizeName = "innerW"; 32437 beforeName = 'left'; 32438 deltaSizeName = "deltaW"; 32439 contentSizeName = "contentW"; 32440 32441 alignBeforeName = "top"; 32442 alignSizeName = "h"; 32443 alignAxisName = "y"; 32444 alignInnerSizeName = "innerH"; 32445 alignMinSizeName = "minH"; 32446 alignAfterName = "bottom"; 32447 alignDeltaSizeName = "deltaH"; 32448 alignContentSizeName = "contentH"; 32449 } 32450 32451 // Figure out total flex, availableSpace and collect any max size elements 32452 availableSpace = contLayoutRect[innerSizeName] - contPaddingBox[beforeName] - contPaddingBox[beforeName]; 32453 maxAlignEndPos = totalFlex = 0; 32454 for (i = 0, l = items.length; i < l; i++) { 32455 ctrl = items[i]; 32456 ctrlLayoutRect = ctrl.layoutRect(); 32457 ctrlSettings = ctrl.settings; 32458 flex = ctrlSettings.flex; 32459 availableSpace -= (i < l - 1 ? spacing : 0); 32460 32461 if (flex > 0) { 32462 totalFlex += flex; 32463 32464 // Flexed item has a max size then we need to check if we will hit that size 32465 if (ctrlLayoutRect[maxSizeName]) { 32466 maxSizeItems.push(ctrl); 32467 } 32468 32469 ctrlLayoutRect.flex = flex; 32470 } 32471 32472 availableSpace -= ctrlLayoutRect[minSizeName]; 32473 32474 // Calculate the align end position to be used to check for overflow/underflow 32475 size = contPaddingBox[alignBeforeName] + ctrlLayoutRect[alignMinSizeName] + contPaddingBox[alignAfterName]; 32476 if (size > maxAlignEndPos) { 32477 maxAlignEndPos = size; 32478 } 32479 } 32480 32481 // Calculate minW/minH 32482 rect = {}; 32483 if (availableSpace < 0) { 32484 rect[minSizeName] = contLayoutRect[minSizeName] - availableSpace + contLayoutRect[deltaSizeName]; 32485 } else { 32486 rect[minSizeName] = contLayoutRect[innerSizeName] - availableSpace + contLayoutRect[deltaSizeName]; 32487 } 32488 32489 rect[alignMinSizeName] = maxAlignEndPos + contLayoutRect[alignDeltaSizeName]; 32490 32491 rect[contentSizeName] = contLayoutRect[innerSizeName] - availableSpace; 32492 rect[alignContentSizeName] = maxAlignEndPos; 32493 rect.minW = min(rect.minW, contLayoutRect.maxW); 32494 rect.minH = min(rect.minH, contLayoutRect.maxH); 32495 rect.minW = max(rect.minW, contLayoutRect.startMinWidth); 32496 rect.minH = max(rect.minH, contLayoutRect.startMinHeight); 32497 32498 // Resize container container if minSize was changed 32499 if (contLayoutRect.autoResize && (rect.minW != contLayoutRect.minW || rect.minH != contLayoutRect.minH)) { 32500 rect.w = rect.minW; 32501 rect.h = rect.minH; 32502 32503 container.layoutRect(rect); 32504 this.recalc(container); 32505 32506 // Forced recalc for example if items are hidden/shown 32507 if (container._lastRect === null) { 32508 var parentCtrl = container.parent(); 32509 if (parentCtrl) { 32510 parentCtrl._lastRect = null; 32511 parentCtrl.recalc(); 32512 } 32513 } 32514 32515 return; 32516 } 32517 32518 // Handle max size elements, check if they will become to wide with current options 32519 ratio = availableSpace / totalFlex; 32520 for (i = 0, l = maxSizeItems.length; i < l; i++) { 32521 ctrl = maxSizeItems[i]; 32522 ctrlLayoutRect = ctrl.layoutRect(); 32523 maxSize = ctrlLayoutRect[maxSizeName]; 32524 size = ctrlLayoutRect[minSizeName] + ctrlLayoutRect.flex * ratio; 32525 32526 if (size > maxSize) { 32527 availableSpace -= (ctrlLayoutRect[maxSizeName] - ctrlLayoutRect[minSizeName]); 32528 totalFlex -= ctrlLayoutRect.flex; 32529 ctrlLayoutRect.flex = 0; 32530 ctrlLayoutRect.maxFlexSize = maxSize; 32531 } else { 32532 ctrlLayoutRect.maxFlexSize = 0; 32533 } 32534 } 32535 32536 // Setup new ratio, target layout rect, start position 32537 ratio = availableSpace / totalFlex; 32538 pos = contPaddingBox[beforeName]; 32539 rect = {}; 32540 32541 // Handle pack setting moves the start position to end, center 32542 if (totalFlex === 0) { 32543 if (pack == "end") { 32544 pos = availableSpace + contPaddingBox[beforeName]; 32545 } else if (pack == "center") { 32546 pos = Math.round( 32547 (contLayoutRect[innerSizeName] / 2) - ((contLayoutRect[innerSizeName] - availableSpace) / 2) 32548 ) + contPaddingBox[beforeName]; 32549 32550 if (pos < 0) { 32551 pos = contPaddingBox[beforeName]; 32552 } 32553 } else if (pack == "justify") { 32554 pos = contPaddingBox[beforeName]; 32555 spacing = Math.floor(availableSpace / (items.length - 1)); 32556 } 32557 } 32558 32559 // Default aligning (start) the other ones needs to be calculated while doing the layout 32560 rect[alignAxisName] = contPaddingBox[alignBeforeName]; 32561 32562 // Start laying out controls 32563 for (i = 0, l = items.length; i < l; i++) { 32564 ctrl = items[i]; 32565 ctrlLayoutRect = ctrl.layoutRect(); 32566 size = ctrlLayoutRect.maxFlexSize || ctrlLayoutRect[minSizeName]; 32567 32568 // Align the control on the other axis 32569 if (align === "center") { 32570 rect[alignAxisName] = Math.round((contLayoutRect[alignInnerSizeName] / 2) - (ctrlLayoutRect[alignSizeName] / 2)); 32571 } else if (align === "stretch") { 32572 rect[alignSizeName] = max( 32573 ctrlLayoutRect[alignMinSizeName] || 0, 32574 contLayoutRect[alignInnerSizeName] - contPaddingBox[alignBeforeName] - contPaddingBox[alignAfterName] 32575 ); 32576 rect[alignAxisName] = contPaddingBox[alignBeforeName]; 32577 } else if (align === "end") { 32578 rect[alignAxisName] = contLayoutRect[alignInnerSizeName] - ctrlLayoutRect[alignSizeName] - contPaddingBox.top; 32579 } 32580 32581 // Calculate new size based on flex 32582 if (ctrlLayoutRect.flex > 0) { 32583 size += ctrlLayoutRect.flex * ratio; 32584 } 32585 32586 rect[sizeName] = size; 32587 rect[posName] = pos; 32588 ctrl.layoutRect(rect); 32589 32590 // Recalculate containers 32591 if (ctrl.recalc) { 32592 ctrl.recalc(); 32593 } 32594 32595 // Move x/y position 32596 pos += size + spacing; 32597 } 32598 } 32599 }); 32600 }); 32601 32602 // Included from: js/tinymce/classes/ui/FlowLayout.js 32603 32604 /** 32605 * FlowLayout.js 32606 * 32607 * Copyright, Moxiecode Systems AB 32608 * Released under LGPL License. 32609 * 32610 * License: http://www.tinymce.com/license 32611 * Contributing: http://www.tinymce.com/contributing 32612 */ 32613 32614 /** 32615 * This layout manager will place the controls by using the browsers native layout. 32616 * 32617 * @-x-less FlowLayout.less 32618 * @class tinymce.ui.FlowLayout 32619 * @extends tinymce.ui.Layout 32620 */ 32621 define("tinymce/ui/FlowLayout", [ 32622 "tinymce/ui/Layout" 32623 ], function(Layout) { 32624 return Layout.extend({ 32625 Defaults: { 32626 containerClass: 'flow-layout', 32627 controlClass: 'flow-layout-item', 32628 endClass : 'break' 32629 }, 32630 32631 /** 32632 * Recalculates the positions of the controls in the specified container. 32633 * 32634 * @method recalc 32635 * @param {tinymce.ui.Container} container Container instance to recalc. 32636 */ 32637 recalc: function(container) { 32638 container.items().filter(':visible').each(function(ctrl) { 32639 if (ctrl.recalc) { 32640 ctrl.recalc(); 32641 } 32642 }); 32643 } 32644 }); 32645 }); 32646 32647 // Included from: js/tinymce/classes/ui/FormatControls.js 32648 32649 /** 32650 * FormatControls.js 32651 * 32652 * Copyright, Moxiecode Systems AB 32653 * Released under LGPL License. 32654 * 32655 * License: http://www.tinymce.com/license 32656 * Contributing: http://www.tinymce.com/contributing 32657 */ 32658 32659 /** 32660 * Internal class containing all TinyMCE specific control types such as 32661 * format listboxes, fontlist boxes, toolbar buttons etc. 32662 * 32663 * @class tinymce.ui.FormatControls 32664 */ 32665 define("tinymce/ui/FormatControls", [ 32666 "tinymce/ui/Control", 32667 "tinymce/ui/Widget", 32668 "tinymce/ui/FloatPanel", 32669 "tinymce/util/Tools", 32670 "tinymce/EditorManager", 32671 "tinymce/Env" 32672 ], function(Control, Widget, FloatPanel, Tools, EditorManager, Env) { 32673 var each = Tools.each; 32674 32675 EditorManager.on('AddEditor', function(e) { 32676 if (e.editor.rtl) { 32677 Control.rtl = true; 32678 } 32679 32680 registerControls(e.editor); 32681 }); 32682 32683 Control.translate = function(text) { 32684 return EditorManager.translate(text); 32685 }; 32686 32687 Widget.tooltips = !Env.iOS; 32688 32689 function registerControls(editor) { 32690 var formatMenu; 32691 32692 function createListBoxChangeHandler(items, formatName) { 32693 return function() { 32694 var self = this; 32695 32696 editor.on('nodeChange', function(e) { 32697 var formatter = editor.formatter; 32698 var value = null; 32699 32700 each(e.parents, function(node) { 32701 each(items, function(item) { 32702 if (formatName) { 32703 if (formatter.matchNode(node, formatName, {value: item.value})) { 32704 value = item.value; 32705 } 32706 } else { 32707 if (formatter.matchNode(node, item.value)) { 32708 value = item.value; 32709 } 32710 } 32711 32712 if (value) { 32713 return false; 32714 } 32715 }); 32716 32717 if (value) { 32718 return false; 32719 } 32720 }); 32721 32722 self.value(value); 32723 }); 32724 }; 32725 } 32726 32727 function createFormats(formats) { 32728 formats = formats.replace(/;$/, '').split(';'); 32729 32730 var i = formats.length; 32731 while (i--) { 32732 formats[i] = formats[i].split('='); 32733 } 32734 32735 return formats; 32736 } 32737 32738 function createFormatMenu() { 32739 var count = 0, newFormats = []; 32740 32741 var defaultStyleFormats = [ 32742 {title: 'Headings', items: [ 32743 {title: 'Heading 1', format: 'h1'}, 32744 {title: 'Heading 2', format: 'h2'}, 32745 {title: 'Heading 3', format: 'h3'}, 32746 {title: 'Heading 4', format: 'h4'}, 32747 {title: 'Heading 5', format: 'h5'}, 32748 {title: 'Heading 6', format: 'h6'} 32749 ]}, 32750 32751 {title: 'Inline', items: [ 32752 {title: 'Bold', icon: 'bold', format: 'bold'}, 32753 {title: 'Italic', icon: 'italic', format: 'italic'}, 32754 {title: 'Underline', icon: 'underline', format: 'underline'}, 32755 {title: 'Strikethrough', icon: 'strikethrough', format: 'strikethrough'}, 32756 {title: 'Superscript', icon: 'superscript', format: 'superscript'}, 32757 {title: 'Subscript', icon: 'subscript', format: 'subscript'}, 32758 {title: 'Code', icon: 'code', format: 'code'} 32759 ]}, 32760 32761 {title: 'Blocks', items: [ 32762 {title: 'Paragraph', format: 'p'}, 32763 {title: 'Blockquote', format: 'blockquote'}, 32764 {title: 'Div', format: 'div'}, 32765 {title: 'Pre', format: 'pre'} 32766 ]}, 32767 32768 {title: 'Alignment', items: [ 32769 {title: 'Left', icon: 'alignleft', format: 'alignleft'}, 32770 {title: 'Center', icon: 'aligncenter', format: 'aligncenter'}, 32771 {title: 'Right', icon: 'alignright', format: 'alignright'}, 32772 {title: 'Justify', icon: 'alignjustify', format: 'alignjustify'} 32773 ]} 32774 ]; 32775 32776 function createMenu(formats) { 32777 var menu = []; 32778 32779 if (!formats) { 32780 return; 32781 } 32782 32783 each(formats, function(format) { 32784 var menuItem = { 32785 text: format.title, 32786 icon: format.icon 32787 }; 32788 32789 if (format.items) { 32790 menuItem.menu = createMenu(format.items); 32791 } else { 32792 var formatName = format.format || "custom" + count++; 32793 32794 if (!format.format) { 32795 format.name = formatName; 32796 newFormats.push(format); 32797 } 32798 32799 menuItem.format = formatName; 32800 } 32801 32802 menu.push(menuItem); 32803 }); 32804 32805 return menu; 32806 } 32807 32808 function createStylesMenu() { 32809 var menu; 32810 32811 if (editor.settings.style_formats_merge) { 32812 if (editor.settings.style_formats) { 32813 menu = createMenu(defaultStyleFormats.concat(editor.settings.style_formats)); 32814 } else { 32815 menu = createMenu(defaultStyleFormats); 32816 } 32817 } else { 32818 menu = createMenu(editor.settings.style_formats || defaultStyleFormats); 32819 } 32820 32821 return menu; 32822 } 32823 32824 editor.on('init', function() { 32825 each(newFormats, function(format) { 32826 editor.formatter.register(format.name, format); 32827 }); 32828 }); 32829 32830 return { 32831 type: 'menu', 32832 items: createStylesMenu(), 32833 onPostRender: function(e) { 32834 editor.fire('renderFormatsMenu', {control: e.control}); 32835 }, 32836 itemDefaults: { 32837 preview: true, 32838 32839 textStyle: function() { 32840 if (this.settings.format) { 32841 return editor.formatter.getCssText(this.settings.format); 32842 } 32843 }, 32844 32845 onPostRender: function() { 32846 var self = this, formatName = this.settings.format; 32847 32848 if (formatName) { 32849 self.parent().on('show', function() { 32850 self.disabled(!editor.formatter.canApply(formatName)); 32851 self.active(editor.formatter.match(formatName)); 32852 }); 32853 } 32854 }, 32855 32856 onclick: function() { 32857 if (this.settings.format) { 32858 toggleFormat(this.settings.format); 32859 } 32860 } 32861 } 32862 }; 32863 } 32864 32865 formatMenu = createFormatMenu(); 32866 32867 // Simple format controls <control/format>:<UI text> 32868 each({ 32869 bold: 'Bold', 32870 italic: 'Italic', 32871 underline: 'Underline', 32872 strikethrough: 'Strikethrough', 32873 subscript: 'Subscript', 32874 superscript: 'Superscript' 32875 }, function(text, name) { 32876 editor.addButton(name, { 32877 tooltip: text, 32878 onPostRender: function() { 32879 var self = this; 32880 32881 // TODO: Fix this 32882 if (editor.formatter) { 32883 editor.formatter.formatChanged(name, function(state) { 32884 self.active(state); 32885 }); 32886 } else { 32887 editor.on('init', function() { 32888 editor.formatter.formatChanged(name, function(state) { 32889 self.active(state); 32890 }); 32891 }); 32892 } 32893 }, 32894 onclick: function() { 32895 toggleFormat(name); 32896 } 32897 }); 32898 }); 32899 32900 // Simple command controls <control>:[<UI text>,<Command>] 32901 each({ 32902 outdent: ['Decrease indent', 'Outdent'], 32903 indent: ['Increase indent', 'Indent'], 32904 cut: ['Cut', 'Cut'], 32905 copy: ['Copy', 'Copy'], 32906 paste: ['Paste', 'Paste'], 32907 help: ['Help', 'mceHelp'], 32908 selectall: ['Select all', 'SelectAll'], 32909 hr: ['Insert horizontal rule', 'InsertHorizontalRule'], 32910 removeformat: ['Clear formatting', 'RemoveFormat'], 32911 visualaid: ['Visual aids', 'mceToggleVisualAid'], 32912 newdocument: ['New document', 'mceNewDocument'] 32913 }, function(item, name) { 32914 editor.addButton(name, { 32915 tooltip: item[0], 32916 cmd: item[1] 32917 }); 32918 }); 32919 32920 // Simple command controls with format state 32921 each({ 32922 blockquote: ['Blockquote', 'mceBlockQuote'], 32923 numlist: ['Numbered list', 'InsertOrderedList'], 32924 bullist: ['Bullet list', 'InsertUnorderedList'], 32925 subscript: ['Subscript', 'Subscript'], 32926 superscript: ['Superscript', 'Superscript'], 32927 alignleft: ['Align left', 'JustifyLeft'], 32928 aligncenter: ['Align center', 'JustifyCenter'], 32929 alignright: ['Align right', 'JustifyRight'], 32930 alignjustify: ['Justify', 'JustifyFull'] 32931 }, function(item, name) { 32932 editor.addButton(name, { 32933 tooltip: item[0], 32934 cmd: item[1], 32935 onPostRender: function() { 32936 var self = this; 32937 32938 // TODO: Fix this 32939 if (editor.formatter) { 32940 editor.formatter.formatChanged(name, function(state) { 32941 self.active(state); 32942 }); 32943 } else { 32944 editor.on('init', function() { 32945 editor.formatter.formatChanged(name, function(state) { 32946 self.active(state); 32947 }); 32948 }); 32949 } 32950 } 32951 }); 32952 }); 32953 32954 function hasUndo() { 32955 return editor.undoManager ? editor.undoManager.hasUndo() : false; 32956 } 32957 32958 function hasRedo() { 32959 return editor.undoManager ? editor.undoManager.hasRedo() : false; 32960 } 32961 32962 function toggleUndoState() { 32963 var self = this; 32964 32965 self.disabled(!hasUndo()); 32966 editor.on('Undo Redo AddUndo TypingUndo', function() { 32967 self.disabled(!hasUndo()); 32968 }); 32969 } 32970 32971 function toggleRedoState() { 32972 var self = this; 32973 32974 self.disabled(!hasRedo()); 32975 editor.on('Undo Redo AddUndo TypingUndo', function() { 32976 self.disabled(!hasRedo()); 32977 }); 32978 } 32979 32980 function toggleVisualAidState() { 32981 var self = this; 32982 32983 editor.on('VisualAid', function(e) { 32984 self.active(e.hasVisual); 32985 }); 32986 32987 self.active(editor.hasVisual); 32988 } 32989 32990 editor.addButton('undo', { 32991 tooltip: 'Undo', 32992 onPostRender: toggleUndoState, 32993 cmd: 'undo' 32994 }); 32995 32996 editor.addButton('redo', { 32997 tooltip: 'Redo', 32998 onPostRender: toggleRedoState, 32999 cmd: 'redo' 33000 }); 33001 33002 editor.addMenuItem('newdocument', { 33003 text: 'New document', 33004 shortcut: 'Ctrl+N', 33005 icon: 'newdocument', 33006 cmd: 'mceNewDocument' 33007 }); 33008 33009 editor.addMenuItem('undo', { 33010 text: 'Undo', 33011 icon: 'undo', 33012 shortcut: 'Ctrl+Z', 33013 onPostRender: toggleUndoState, 33014 cmd: 'undo' 33015 }); 33016 33017 editor.addMenuItem('redo', { 33018 text: 'Redo', 33019 icon: 'redo', 33020 shortcut: 'Ctrl+Y', 33021 onPostRender: toggleRedoState, 33022 cmd: 'redo' 33023 }); 33024 33025 editor.addMenuItem('visualaid', { 33026 text: 'Visual aids', 33027 selectable: true, 33028 onPostRender: toggleVisualAidState, 33029 cmd: 'mceToggleVisualAid' 33030 }); 33031 33032 each({ 33033 cut: ['Cut', 'Cut', 'Ctrl+X'], 33034 copy: ['Copy', 'Copy', 'Ctrl+C'], 33035 paste: ['Paste', 'Paste', 'Ctrl+V'], 33036 selectall: ['Select all', 'SelectAll', 'Ctrl+A'], 33037 bold: ['Bold', 'Bold', 'Ctrl+B'], 33038 italic: ['Italic', 'Italic', 'Ctrl+I'], 33039 underline: ['Underline', 'Underline'], 33040 strikethrough: ['Strikethrough', 'Strikethrough'], 33041 subscript: ['Subscript', 'Subscript'], 33042 superscript: ['Superscript', 'Superscript'], 33043 removeformat: ['Clear formatting', 'RemoveFormat'] 33044 }, function(item, name) { 33045 editor.addMenuItem(name, { 33046 text: item[0], 33047 icon: name, 33048 shortcut: item[2], 33049 cmd: item[1] 33050 }); 33051 }); 33052 33053 editor.on('mousedown', function() { 33054 FloatPanel.hideAll(); 33055 }); 33056 33057 function toggleFormat(fmt) { 33058 if (fmt.control) { 33059 fmt = fmt.control.value(); 33060 } 33061 33062 if (fmt) { 33063 editor.execCommand('mceToggleFormat', false, fmt); 33064 } 33065 } 33066 33067 editor.addButton('styleselect', { 33068 type: 'menubutton', 33069 text: 'Formats', 33070 menu: formatMenu 33071 }); 33072 33073 editor.addButton('formatselect', function() { 33074 var items = [], blocks = createFormats(editor.settings.block_formats || 33075 'Paragraph=p;' + 33076 'Address=address;' + 33077 'Pre=pre;' + 33078 'Heading 1=h1;' + 33079 'Heading 2=h2;' + 33080 'Heading 3=h3;' + 33081 'Heading 4=h4;' + 33082 'Heading 5=h5;' + 33083 'Heading 6=h6' 33084 ); 33085 33086 each(blocks, function(block) { 33087 items.push({ 33088 text: block[0], 33089 value: block[1], 33090 textStyle: function() { 33091 return editor.formatter.getCssText(block[1]); 33092 } 33093 }); 33094 }); 33095 33096 return { 33097 type: 'listbox', 33098 text: blocks[0][0], 33099 values: items, 33100 fixedWidth: true, 33101 onselect: toggleFormat, 33102 onPostRender: createListBoxChangeHandler(items) 33103 }; 33104 }); 33105 33106 editor.addButton('fontselect', function() { 33107 var defaultFontsFormats = 33108 'Andale Mono=andale mono,times;' + 33109 'Arial=arial,helvetica,sans-serif;' + 33110 'Arial Black=arial black,avant garde;' + 33111 'Book Antiqua=book antiqua,palatino;' + 33112 'Comic Sans MS=comic sans ms,sans-serif;' + 33113 'Courier New=courier new,courier;' + 33114 'Georgia=georgia,palatino;' + 33115 'Helvetica=helvetica;' + 33116 'Impact=impact,chicago;' + 33117 'Symbol=symbol;' + 33118 'Tahoma=tahoma,arial,helvetica,sans-serif;' + 33119 'Terminal=terminal,monaco;' + 33120 'Times New Roman=times new roman,times;' + 33121 'Trebuchet MS=trebuchet ms,geneva;' + 33122 'Verdana=verdana,geneva;' + 33123 'Webdings=webdings;' + 33124 'Wingdings=wingdings,zapf dingbats'; 33125 33126 var items = [], fonts = createFormats(editor.settings.font_formats || defaultFontsFormats); 33127 33128 each(fonts, function(font) { 33129 items.push({ 33130 text: {raw: font[0]}, 33131 value: font[1], 33132 textStyle: font[1].indexOf('dings') == -1 ? 'font-family:' + font[1] : '' 33133 }); 33134 }); 33135 33136 return { 33137 type: 'listbox', 33138 text: 'Font Family', 33139 tooltip: 'Font Family', 33140 values: items, 33141 fixedWidth: true, 33142 onPostRender: createListBoxChangeHandler(items, 'fontname'), 33143 onselect: function(e) { 33144 if (e.control.settings.value) { 33145 editor.execCommand('FontName', false, e.control.settings.value); 33146 } 33147 } 33148 }; 33149 }); 33150 33151 editor.addButton('fontsizeselect', function() { 33152 var items = [], defaultFontsizeFormats = '8pt 10pt 12pt 14pt 18pt 24pt 36pt'; 33153 var fontsize_formats = editor.settings.fontsize_formats || defaultFontsizeFormats; 33154 33155 each(fontsize_formats.split(' '), function(item) { 33156 items.push({text: item, value: item}); 33157 }); 33158 33159 return { 33160 type: 'listbox', 33161 text: 'Font Sizes', 33162 tooltip: 'Font Sizes', 33163 values: items, 33164 fixedWidth: true, 33165 onPostRender: createListBoxChangeHandler(items, 'fontsize'), 33166 onclick: function(e) { 33167 if (e.control.settings.value) { 33168 editor.execCommand('FontSize', false, e.control.settings.value); 33169 } 33170 } 33171 }; 33172 }); 33173 33174 editor.addMenuItem('formats', { 33175 text: 'Formats', 33176 menu: formatMenu 33177 }); 33178 } 33179 }); 33180 33181 // Included from: js/tinymce/classes/ui/GridLayout.js 33182 33183 /** 33184 * GridLayout.js 33185 * 33186 * Copyright, Moxiecode Systems AB 33187 * Released under LGPL License. 33188 * 33189 * License: http://www.tinymce.com/license 33190 * Contributing: http://www.tinymce.com/contributing 33191 */ 33192 33193 /** 33194 * This layout manager places controls in a grid. 33195 * 33196 * @setting {Number} spacing Spacing between controls. 33197 * @setting {Number} spacingH Horizontal spacing between controls. 33198 * @setting {Number} spacingV Vertical spacing between controls. 33199 * @setting {Number} columns Number of columns to use. 33200 * @setting {String/Array} alignH start|end|center|stretch or array of values for each column. 33201 * @setting {String/Array} alignV start|end|center|stretch or array of values for each column. 33202 * @setting {String} pack start|end 33203 * 33204 * @class tinymce.ui.GridLayout 33205 * @extends tinymce.ui.AbsoluteLayout 33206 */ 33207 define("tinymce/ui/GridLayout", [ 33208 "tinymce/ui/AbsoluteLayout" 33209 ], function(AbsoluteLayout) { 33210 "use strict"; 33211 33212 return AbsoluteLayout.extend({ 33213 /** 33214 * Recalculates the positions of the controls in the specified container. 33215 * 33216 * @method recalc 33217 * @param {tinymce.ui.Container} container Container instance to recalc. 33218 */ 33219 recalc: function(container) { 33220 var settings = container.settings, rows, cols, items, contLayoutRect, width, height, rect, 33221 ctrlLayoutRect, ctrl, x, y, posX, posY, ctrlSettings, contPaddingBox, align, spacingH, spacingV, alignH, alignV, maxX, maxY, 33222 colWidths = [], rowHeights = [], ctrlMinWidth, ctrlMinHeight, availableWidth, availableHeight; 33223 33224 // Get layout settings 33225 settings = container.settings; 33226 items = container.items().filter(':visible'); 33227 contLayoutRect = container.layoutRect(); 33228 cols = settings.columns || Math.ceil(Math.sqrt(items.length)); 33229 rows = Math.ceil(items.length / cols); 33230 spacingH = settings.spacingH || settings.spacing || 0; 33231 spacingV = settings.spacingV || settings.spacing || 0; 33232 alignH = settings.alignH || settings.align; 33233 alignV = settings.alignV || settings.align; 33234 contPaddingBox = container._paddingBox; 33235 33236 if (alignH && typeof(alignH) == "string") { 33237 alignH = [alignH]; 33238 } 33239 33240 if (alignV && typeof(alignV) == "string") { 33241 alignV = [alignV]; 33242 } 33243 33244 // Zero padd columnWidths 33245 for (x = 0; x < cols; x++) { 33246 colWidths.push(0); 33247 } 33248 33249 // Zero padd rowHeights 33250 for (y = 0; y < rows; y++) { 33251 rowHeights.push(0); 33252 } 33253 33254 // Calculate columnWidths and rowHeights 33255 for (y = 0; y < rows; y++) { 33256 for (x = 0; x < cols; x++) { 33257 ctrl = items[y * cols + x]; 33258 33259 // Out of bounds 33260 if (!ctrl) { 33261 break; 33262 } 33263 33264 ctrlLayoutRect = ctrl.layoutRect(); 33265 ctrlMinWidth = ctrlLayoutRect.minW; 33266 ctrlMinHeight = ctrlLayoutRect.minH; 33267 33268 colWidths[x] = ctrlMinWidth > colWidths[x] ? ctrlMinWidth : colWidths[x]; 33269 rowHeights[y] = ctrlMinHeight > rowHeights[y] ? ctrlMinHeight : rowHeights[y]; 33270 } 33271 } 33272 33273 // Calculate maxX 33274 availableWidth = contLayoutRect.innerW - contPaddingBox.left - contPaddingBox.right; 33275 for (maxX = 0, x = 0; x < cols; x++) { 33276 maxX += colWidths[x] + (x > 0 ? spacingH : 0); 33277 availableWidth -= (x > 0 ? spacingH : 0) + colWidths[x]; 33278 } 33279 33280 // Calculate maxY 33281 availableHeight = contLayoutRect.innerH - contPaddingBox.top - contPaddingBox.bottom; 33282 for (maxY = 0, y = 0; y < rows; y++) { 33283 maxY += rowHeights[y] + (y > 0 ? spacingV : 0); 33284 availableHeight -= (y > 0 ? spacingV : 0) + rowHeights[y]; 33285 } 33286 33287 maxX += contPaddingBox.left + contPaddingBox.right; 33288 maxY += contPaddingBox.top + contPaddingBox.bottom; 33289 33290 // Calculate minW/minH 33291 rect = {}; 33292 rect.minW = maxX + (contLayoutRect.w - contLayoutRect.innerW); 33293 rect.minH = maxY + (contLayoutRect.h - contLayoutRect.innerH); 33294 33295 rect.contentW = rect.minW - contLayoutRect.deltaW; 33296 rect.contentH = rect.minH - contLayoutRect.deltaH; 33297 rect.minW = Math.min(rect.minW, contLayoutRect.maxW); 33298 rect.minH = Math.min(rect.minH, contLayoutRect.maxH); 33299 rect.minW = Math.max(rect.minW, contLayoutRect.startMinWidth); 33300 rect.minH = Math.max(rect.minH, contLayoutRect.startMinHeight); 33301 33302 // Resize container container if minSize was changed 33303 if (contLayoutRect.autoResize && (rect.minW != contLayoutRect.minW || rect.minH != contLayoutRect.minH)) { 33304 rect.w = rect.minW; 33305 rect.h = rect.minH; 33306 33307 container.layoutRect(rect); 33308 this.recalc(container); 33309 33310 // Forced recalc for example if items are hidden/shown 33311 if (container._lastRect === null) { 33312 var parentCtrl = container.parent(); 33313 if (parentCtrl) { 33314 parentCtrl._lastRect = null; 33315 parentCtrl.recalc(); 33316 } 33317 } 33318 33319 return; 33320 } 33321 33322 // Update contentW/contentH so absEnd moves correctly 33323 if (contLayoutRect.autoResize) { 33324 rect = container.layoutRect(rect); 33325 rect.contentW = rect.minW - contLayoutRect.deltaW; 33326 rect.contentH = rect.minH - contLayoutRect.deltaH; 33327 } 33328 33329 var flexV; 33330 33331 if (settings.packV == 'start') { 33332 flexV = 0; 33333 } else { 33334 flexV = availableHeight > 0 ? Math.floor(availableHeight / rows) : 0; 33335 } 33336 33337 // Calculate totalFlex 33338 var totalFlex = 0; 33339 var flexWidths = settings.flexWidths; 33340 if (flexWidths) { 33341 for (x = 0; x < flexWidths.length; x++) { 33342 totalFlex += flexWidths[x]; 33343 } 33344 } else { 33345 totalFlex = cols; 33346 } 33347 33348 // Calculate new column widths based on flex values 33349 var ratio = availableWidth / totalFlex; 33350 for (x = 0; x < cols; x++) { 33351 colWidths[x] += flexWidths ? flexWidths[x] * ratio : ratio; 33352 } 33353 33354 // Move/resize controls 33355 posY = contPaddingBox.top; 33356 for (y = 0; y < rows; y++) { 33357 posX = contPaddingBox.left; 33358 height = rowHeights[y] + flexV; 33359 33360 for (x = 0; x < cols; x++) { 33361 ctrl = items[y * cols + x]; 33362 33363 // No more controls to render then break 33364 if (!ctrl) { 33365 break; 33366 } 33367 33368 // Get control settings and calculate x, y 33369 ctrlSettings = ctrl.settings; 33370 ctrlLayoutRect = ctrl.layoutRect(); 33371 width = Math.max(colWidths[x], ctrlLayoutRect.startMinWidth); 33372 ctrlLayoutRect.x = posX; 33373 ctrlLayoutRect.y = posY; 33374 33375 // Align control horizontal 33376 align = ctrlSettings.alignH || (alignH ? (alignH[x] || alignH[0]) : null); 33377 if (align == "center") { 33378 ctrlLayoutRect.x = posX + (width / 2) - (ctrlLayoutRect.w / 2); 33379 } else if (align == "right") { 33380 ctrlLayoutRect.x = posX + width - ctrlLayoutRect.w; 33381 } else if (align == "stretch") { 33382 ctrlLayoutRect.w = width; 33383 } 33384 33385 // Align control vertical 33386 align = ctrlSettings.alignV || (alignV ? (alignV[x] || alignV[0]) : null); 33387 if (align == "center") { 33388 ctrlLayoutRect.y = posY + (height / 2) - (ctrlLayoutRect.h / 2); 33389 } else if (align == "bottom") { 33390 ctrlLayoutRect.y = posY + height - ctrlLayoutRect.h; 33391 } else if (align == "stretch") { 33392 ctrlLayoutRect.h = height; 33393 } 33394 33395 ctrl.layoutRect(ctrlLayoutRect); 33396 33397 posX += width + spacingH; 33398 33399 if (ctrl.recalc) { 33400 ctrl.recalc(); 33401 } 33402 } 33403 33404 posY += height + spacingV; 33405 } 33406 } 33407 }); 33408 }); 33409 33410 // Included from: js/tinymce/classes/ui/Iframe.js 33411 33412 /** 33413 * Iframe.js 33414 * 33415 * Copyright, Moxiecode Systems AB 33416 * Released under LGPL License. 33417 * 33418 * License: http://www.tinymce.com/license 33419 * Contributing: http://www.tinymce.com/contributing 33420 */ 33421 33422 /*jshint scripturl:true */ 33423 33424 /** 33425 * This class creates an iframe. 33426 * 33427 * @setting {String} url Url to open in the iframe. 33428 * 33429 * @-x-less Iframe.less 33430 * @class tinymce.ui.Iframe 33431 * @extends tinymce.ui.Widget 33432 */ 33433 define("tinymce/ui/Iframe", [ 33434 "tinymce/ui/Widget" 33435 ], function(Widget) { 33436 "use strict"; 33437 33438 return Widget.extend({ 33439 /** 33440 * Renders the control as a HTML string. 33441 * 33442 * @method renderHtml 33443 * @return {String} HTML representing the control. 33444 */ 33445 renderHtml: function() { 33446 var self = this; 33447 33448 self.addClass('iframe'); 33449 self.canFocus = false; 33450 33451 /*eslint no-script-url:0 */ 33452 return ( 33453 '<iframe id="' + self._id + '" class="' + self.classes() + '" tabindex="-1" src="' + 33454 (self.settings.url || "javascript:\'\'") + '" frameborder="0"></iframe>' 33455 ); 33456 }, 33457 33458 /** 33459 * Setter for the iframe source. 33460 * 33461 * @method src 33462 * @param {String} src Source URL for iframe. 33463 */ 33464 src: function(src) { 33465 this.getEl().src = src; 33466 }, 33467 33468 /** 33469 * Inner HTML for the iframe. 33470 * 33471 * @method html 33472 * @param {String} html HTML string to set as HTML inside the iframe. 33473 * @param {function} callback Optional callback to execute when the iframe body is filled with contents. 33474 * @return {tinymce.ui.Iframe} Current iframe control. 33475 */ 33476 html: function(html, callback) { 33477 var self = this, body = this.getEl().contentWindow.document.body; 33478 33479 // Wait for iframe to initialize IE 10 takes time 33480 if (!body) { 33481 setTimeout(function() { 33482 self.html(html); 33483 }, 0); 33484 } else { 33485 body.innerHTML = html; 33486 33487 if (callback) { 33488 callback(); 33489 } 33490 } 33491 33492 return this; 33493 } 33494 }); 33495 }); 33496 33497 // Included from: js/tinymce/classes/ui/Label.js 33498 33499 /** 33500 * Label.js 33501 * 33502 * Copyright, Moxiecode Systems AB 33503 * Released under LGPL License. 33504 * 33505 * License: http://www.tinymce.com/license 33506 * Contributing: http://www.tinymce.com/contributing 33507 */ 33508 33509 /** 33510 * This class creates a label element. A label is a simple text control 33511 * that can be bound to other controls. 33512 * 33513 * @-x-less Label.less 33514 * @class tinymce.ui.Label 33515 * @extends tinymce.ui.Widget 33516 */ 33517 define("tinymce/ui/Label", [ 33518 "tinymce/ui/Widget", 33519 "tinymce/ui/DomUtils" 33520 ], function(Widget, DomUtils) { 33521 "use strict"; 33522 33523 return Widget.extend({ 33524 /** 33525 * Constructs a instance with the specified settings. 33526 * 33527 * @constructor 33528 * @param {Object} settings Name/value object with settings. 33529 * @param {Boolean} multiline Multiline label. 33530 */ 33531 init: function(settings) { 33532 var self = this; 33533 33534 self._super(settings); 33535 self.addClass('widget'); 33536 self.addClass('label'); 33537 self.canFocus = false; 33538 33539 if (settings.multiline) { 33540 self.addClass('autoscroll'); 33541 } 33542 33543 if (settings.strong) { 33544 self.addClass('strong'); 33545 } 33546 }, 33547 33548 /** 33549 * Initializes the current controls layout rect. 33550 * This will be executed by the layout managers to determine the 33551 * default minWidth/minHeight etc. 33552 * 33553 * @method initLayoutRect 33554 * @return {Object} Layout rect instance. 33555 */ 33556 initLayoutRect: function() { 33557 var self = this, layoutRect = self._super(); 33558 33559 if (self.settings.multiline) { 33560 var size = DomUtils.getSize(self.getEl()); 33561 33562 // Check if the text fits within maxW if not then try word wrapping it 33563 if (size.width > layoutRect.maxW) { 33564 layoutRect.minW = layoutRect.maxW; 33565 self.addClass('multiline'); 33566 } 33567 33568 self.getEl().style.width = layoutRect.minW + 'px'; 33569 layoutRect.startMinH = layoutRect.h = layoutRect.minH = Math.min(layoutRect.maxH, DomUtils.getSize(self.getEl()).height); 33570 } 33571 33572 return layoutRect; 33573 }, 33574 33575 /** 33576 * Repaints the control after a layout operation. 33577 * 33578 * @method repaint 33579 */ 33580 repaint: function() { 33581 var self = this; 33582 33583 if (!self.settings.multiline) { 33584 self.getEl().style.lineHeight = self.layoutRect().h + 'px'; 33585 } 33586 33587 return self._super(); 33588 }, 33589 33590 /** 33591 * Sets/gets the current label text. 33592 * 33593 * @method text 33594 * @param {String} [text] New label text. 33595 * @return {String|tinymce.ui.Label} Current text or current label instance. 33596 */ 33597 text: function(text) { 33598 var self = this; 33599 33600 if (self._rendered && text) { 33601 this.innerHtml(self.encode(text)); 33602 } 33603 33604 return self._super(text); 33605 }, 33606 33607 /** 33608 * Renders the control as a HTML string. 33609 * 33610 * @method renderHtml 33611 * @return {String} HTML representing the control. 33612 */ 33613 renderHtml: function() { 33614 var self = this, forId = self.settings.forId; 33615 33616 return ( 33617 '<label id="' + self._id + '" class="' + self.classes() + '"' + (forId ? ' for="' + forId + '"' : '') + '>' + 33618 self.encode(self._text) + 33619 '</label>' 33620 ); 33621 } 33622 }); 33623 }); 33624 33625 // Included from: js/tinymce/classes/ui/Toolbar.js 33626 33627 /** 33628 * Toolbar.js 33629 * 33630 * Copyright, Moxiecode Systems AB 33631 * Released under LGPL License. 33632 * 33633 * License: http://www.tinymce.com/license 33634 * Contributing: http://www.tinymce.com/contributing 33635 */ 33636 33637 /** 33638 * Creates a new toolbar. 33639 * 33640 * @class tinymce.ui.Toolbar 33641 * @extends tinymce.ui.Container 33642 */ 33643 define("tinymce/ui/Toolbar", [ 33644 "tinymce/ui/Container" 33645 ], function(Container) { 33646 "use strict"; 33647 33648 return Container.extend({ 33649 Defaults: { 33650 role: 'toolbar', 33651 layout: 'flow' 33652 }, 33653 33654 /** 33655 * Constructs a instance with the specified settings. 33656 * 33657 * @constructor 33658 * @param {Object} settings Name/value object with settings. 33659 */ 33660 init: function(settings) { 33661 var self = this; 33662 33663 self._super(settings); 33664 self.addClass('toolbar'); 33665 }, 33666 33667 /** 33668 * Called after the control has been rendered. 33669 * 33670 * @method postRender 33671 */ 33672 postRender: function() { 33673 var self = this; 33674 33675 self.items().addClass('toolbar-item'); 33676 33677 return self._super(); 33678 } 33679 }); 33680 }); 33681 33682 // Included from: js/tinymce/classes/ui/MenuBar.js 33683 33684 /** 33685 * MenuBar.js 33686 * 33687 * Copyright, Moxiecode Systems AB 33688 * Released under LGPL License. 33689 * 33690 * License: http://www.tinymce.com/license 33691 * Contributing: http://www.tinymce.com/contributing 33692 */ 33693 33694 /** 33695 * Creates a new menubar. 33696 * 33697 * @-x-less MenuBar.less 33698 * @class tinymce.ui.MenuBar 33699 * @extends tinymce.ui.Container 33700 */ 33701 define("tinymce/ui/MenuBar", [ 33702 "tinymce/ui/Toolbar" 33703 ], function(Toolbar) { 33704 "use strict"; 33705 33706 return Toolbar.extend({ 33707 Defaults: { 33708 role: 'menubar', 33709 containerCls: 'menubar', 33710 ariaRoot: true, 33711 defaults: { 33712 type: 'menubutton' 33713 } 33714 } 33715 }); 33716 }); 33717 33718 // Included from: js/tinymce/classes/ui/MenuButton.js 33719 33720 /** 33721 * MenuButton.js 33722 * 33723 * Copyright, Moxiecode Systems AB 33724 * Released under LGPL License. 33725 * 33726 * License: http://www.tinymce.com/license 33727 * Contributing: http://www.tinymce.com/contributing 33728 */ 33729 33730 /** 33731 * Creates a new menu button. 33732 * 33733 * @-x-less MenuButton.less 33734 * @class tinymce.ui.MenuButton 33735 * @extends tinymce.ui.Button 33736 */ 33737 define("tinymce/ui/MenuButton", [ 33738 "tinymce/ui/Button", 33739 "tinymce/ui/Factory", 33740 "tinymce/ui/MenuBar" 33741 ], function(Button, Factory, MenuBar) { 33742 "use strict"; 33743 33744 // TODO: Maybe add as some global function 33745 function isChildOf(node, parent) { 33746 while (node) { 33747 if (parent === node) { 33748 return true; 33749 } 33750 33751 node = node.parentNode; 33752 } 33753 33754 return false; 33755 } 33756 33757 var MenuButton = Button.extend({ 33758 /** 33759 * Constructs a instance with the specified settings. 33760 * 33761 * @constructor 33762 * @param {Object} settings Name/value object with settings. 33763 */ 33764 init: function(settings) { 33765 var self = this; 33766 33767 self._renderOpen = true; 33768 self._super(settings); 33769 33770 self.addClass('menubtn'); 33771 33772 if (settings.fixedWidth) { 33773 self.addClass('fixed-width'); 33774 } 33775 33776 self.aria('haspopup', true); 33777 self.hasPopup = true; 33778 }, 33779 33780 /** 33781 * Shows the menu for the button. 33782 * 33783 * @method showMenu 33784 */ 33785 showMenu: function() { 33786 var self = this, settings = self.settings, menu; 33787 33788 if (self.menu && self.menu.visible()) { 33789 return self.hideMenu(); 33790 } 33791 33792 if (!self.menu) { 33793 menu = settings.menu || []; 33794 33795 // Is menu array then auto constuct menu control 33796 if (menu.length) { 33797 menu = { 33798 type: 'menu', 33799 items: menu 33800 }; 33801 } else { 33802 menu.type = menu.type || 'menu'; 33803 } 33804 33805 self.menu = Factory.create(menu).parent(self).renderTo(); 33806 self.fire('createmenu'); 33807 self.menu.reflow(); 33808 self.menu.on('cancel', function(e) { 33809 if (e.control.parent() === self.menu) { 33810 e.stopPropagation(); 33811 self.focus(); 33812 self.hideMenu(); 33813 } 33814 }); 33815 33816 // Move focus to button when a menu item is selected/clicked 33817 self.menu.on('select', function() { 33818 self.focus(); 33819 }); 33820 33821 self.menu.on('show hide', function(e) { 33822 if (e.control == self.menu) { 33823 self.activeMenu(e.type == 'show'); 33824 } 33825 33826 self.aria('expanded', e.type == 'show'); 33827 }).fire('show'); 33828 } 33829 33830 self.menu.show(); 33831 self.menu.layoutRect({w: self.layoutRect().w}); 33832 self.menu.moveRel(self.getEl(), self.isRtl() ? ['br-tr', 'tr-br'] : ['bl-tl', 'tl-bl']); 33833 }, 33834 33835 /** 33836 * Hides the menu for the button. 33837 * 33838 * @method hideMenu 33839 */ 33840 hideMenu: function() { 33841 var self = this; 33842 33843 if (self.menu) { 33844 self.menu.items().each(function(item) { 33845 if (item.hideMenu) { 33846 item.hideMenu(); 33847 } 33848 }); 33849 33850 self.menu.hide(); 33851 } 33852 }, 33853 33854 /** 33855 * Sets the active menu state. 33856 * 33857 * @private 33858 */ 33859 activeMenu: function(state) { 33860 this.toggleClass('active', state); 33861 }, 33862 33863 /** 33864 * Renders the control as a HTML string. 33865 * 33866 * @method renderHtml 33867 * @return {String} HTML representing the control. 33868 */ 33869 renderHtml: function() { 33870 var self = this, id = self._id, prefix = self.classPrefix; 33871 var icon = self.settings.icon ? prefix + 'ico ' + prefix + 'i-' + self.settings.icon : ''; 33872 33873 self.aria('role', self.parent() instanceof MenuBar ? 'menuitem' : 'button'); 33874 33875 return ( 33876 '<div id="' + id + '" class="' + self.classes() + '" tabindex="-1" aria-labelledby="' + id + '">' + 33877 '<button id="' + id + '-open" role="presentation" type="button" tabindex="-1">' + 33878 (icon ? '<i class="' + icon + '"></i>' : '') + 33879 '<span>' + (self._text ? (icon ? '\u00a0' : '') + self.encode(self._text) : '') + '</span>' + 33880 ' <i class="' + prefix + 'caret"></i>' + 33881 '</button>' + 33882 '</div>' 33883 ); 33884 }, 33885 33886 /** 33887 * Gets invoked after the control has been rendered. 33888 * 33889 * @method postRender 33890 */ 33891 postRender: function() { 33892 var self = this; 33893 33894 self.on('click', function(e) { 33895 if (e.control === self && isChildOf(e.target, self.getEl())) { 33896 self.showMenu(); 33897 33898 if (e.aria) { 33899 self.menu.items()[0].focus(); 33900 } 33901 } 33902 }); 33903 33904 self.on('mouseenter', function(e) { 33905 var overCtrl = e.control, parent = self.parent(), hasVisibleSiblingMenu; 33906 33907 if (overCtrl && parent && overCtrl instanceof MenuButton && overCtrl.parent() == parent) { 33908 parent.items().filter('MenuButton').each(function(ctrl) { 33909 if (ctrl.hideMenu && ctrl != overCtrl) { 33910 if (ctrl.menu && ctrl.menu.visible()) { 33911 hasVisibleSiblingMenu = true; 33912 } 33913 33914 ctrl.hideMenu(); 33915 } 33916 }); 33917 33918 if (hasVisibleSiblingMenu) { 33919 overCtrl.focus(); // Fix for: #5887 33920 overCtrl.showMenu(); 33921 } 33922 } 33923 }); 33924 33925 return self._super(); 33926 }, 33927 33928 /** 33929 * Sets/gets the current button text. 33930 * 33931 * @method text 33932 * @param {String} [text] New button text. 33933 * @return {String|tinymce.ui.MenuButton} Current text or current MenuButton instance. 33934 */ 33935 text: function(text) { 33936 var self = this, i, children; 33937 33938 if (self._rendered) { 33939 children = self.getEl('open').getElementsByTagName('span'); 33940 for (i = 0; i < children.length; i++) { 33941 children[i].innerHTML = (self.settings.icon && text ? '\u00a0' : '') + self.encode(text); 33942 } 33943 } 33944 33945 return this._super(text); 33946 }, 33947 33948 /** 33949 * Removes the control and it's menus. 33950 * 33951 * @method remove 33952 */ 33953 remove: function() { 33954 this._super(); 33955 33956 if (this.menu) { 33957 this.menu.remove(); 33958 } 33959 } 33960 }); 33961 33962 return MenuButton; 33963 }); 33964 33965 // Included from: js/tinymce/classes/ui/ListBox.js 33966 33967 /** 33968 * ListBox.js 33969 * 33970 * Copyright, Moxiecode Systems AB 33971 * Released under LGPL License. 33972 * 33973 * License: http://www.tinymce.com/license 33974 * Contributing: http://www.tinymce.com/contributing 33975 */ 33976 33977 /** 33978 * Creates a new list box control. 33979 * 33980 * @-x-less ListBox.less 33981 * @class tinymce.ui.ListBox 33982 * @extends tinymce.ui.MenuButton 33983 */ 33984 define("tinymce/ui/ListBox", [ 33985 "tinymce/ui/MenuButton" 33986 ], function(MenuButton) { 33987 "use strict"; 33988 33989 return MenuButton.extend({ 33990 /** 33991 * Constructs a instance with the specified settings. 33992 * 33993 * @constructor 33994 * @param {Object} settings Name/value object with settings. 33995 * @setting {Array} values Array with values to add to list box. 33996 */ 33997 init: function(settings) { 33998 var self = this, values, i, selected, selectedText, lastItemCtrl; 33999 34000 self._values = values = settings.values; 34001 if (values) { 34002 for (i = 0; i < values.length; i++) { 34003 selected = values[i].selected || settings.value === values[i].value; 34004 34005 if (selected) { 34006 selectedText = selectedText || values[i].text; 34007 self._value = values[i].value; 34008 break; 34009 } 34010 } 34011 34012 // Default with first item 34013 if (!selected && values.length > 0) { 34014 selectedText = values[0].text; 34015 self._value = values[0].value; 34016 } 34017 34018 settings.menu = values; 34019 } 34020 34021 settings.text = settings.text || selectedText || values[0].text; 34022 34023 self._super(settings); 34024 self.addClass('listbox'); 34025 34026 self.on('select', function(e) { 34027 var ctrl = e.control; 34028 34029 if (lastItemCtrl) { 34030 e.lastControl = lastItemCtrl; 34031 } 34032 34033 if (settings.multiple) { 34034 ctrl.active(!ctrl.active()); 34035 } else { 34036 self.value(e.control.settings.value); 34037 } 34038 34039 lastItemCtrl = ctrl; 34040 }); 34041 }, 34042 34043 /** 34044 * Getter/setter function for the control value. 34045 * 34046 * @method value 34047 * @param {String} [value] Value to be set. 34048 * @return {Boolean/tinymce.ui.ListBox} Value or self if it's a set operation. 34049 */ 34050 value: function(value) { 34051 var self = this, active, selectedText, menu, i; 34052 34053 function activateByValue(menu, value) { 34054 menu.items().each(function(ctrl) { 34055 active = ctrl.value() === value; 34056 34057 if (active) { 34058 selectedText = selectedText || ctrl.text(); 34059 } 34060 34061 ctrl.active(active); 34062 34063 if (ctrl.menu) { 34064 activateByValue(ctrl.menu, value); 34065 } 34066 }); 34067 } 34068 34069 if (typeof(value) != "undefined") { 34070 if (self.menu) { 34071 activateByValue(self.menu, value); 34072 } else { 34073 menu = self.settings.menu; 34074 for (i = 0; i < menu.length; i++) { 34075 active = menu[i].value == value; 34076 34077 if (active) { 34078 selectedText = selectedText || menu[i].text; 34079 } 34080 34081 menu[i].active = active; 34082 } 34083 } 34084 34085 self.text(selectedText || this.settings.text); 34086 } 34087 34088 return self._super(value); 34089 } 34090 }); 34091 }); 34092 34093 // Included from: js/tinymce/classes/ui/MenuItem.js 34094 34095 /** 34096 * MenuItem.js 34097 * 34098 * Copyright, Moxiecode Systems AB 34099 * Released under LGPL License. 34100 * 34101 * License: http://www.tinymce.com/license 34102 * Contributing: http://www.tinymce.com/contributing 34103 */ 34104 34105 /** 34106 * Creates a new menu item. 34107 * 34108 * @-x-less MenuItem.less 34109 * @class tinymce.ui.MenuItem 34110 * @extends tinymce.ui.Control 34111 */ 34112 define("tinymce/ui/MenuItem", [ 34113 "tinymce/ui/Widget", 34114 "tinymce/ui/Factory", 34115 "tinymce/Env" 34116 ], function(Widget, Factory, Env) { 34117 "use strict"; 34118 34119 return Widget.extend({ 34120 Defaults: { 34121 border: 0, 34122 role: 'menuitem' 34123 }, 34124 34125 /** 34126 * Constructs a instance with the specified settings. 34127 * 34128 * @constructor 34129 * @param {Object} settings Name/value object with settings. 34130 * @setting {Boolean} selectable Selectable menu. 34131 * @setting {Array} menu Submenu array with items. 34132 * @setting {String} shortcut Shortcut to display for menu item. Example: Ctrl+X 34133 */ 34134 init: function(settings) { 34135 var self = this; 34136 34137 self.hasPopup = true; 34138 34139 self._super(settings); 34140 34141 settings = self.settings; 34142 34143 self.addClass('menu-item'); 34144 34145 if (settings.menu) { 34146 self.addClass('menu-item-expand'); 34147 } 34148 34149 if (settings.preview) { 34150 self.addClass('menu-item-preview'); 34151 } 34152 34153 if (self._text === '-' || self._text === '|') { 34154 self.addClass('menu-item-sep'); 34155 self.aria('role', 'separator'); 34156 self._text = '-'; 34157 } 34158 34159 if (settings.selectable) { 34160 self.aria('role', 'menuitemcheckbox'); 34161 self.addClass('menu-item-checkbox'); 34162 settings.icon = 'selected'; 34163 } 34164 34165 if (!settings.preview && !settings.selectable) { 34166 self.addClass('menu-item-normal'); 34167 } 34168 34169 self.on('mousedown', function(e) { 34170 e.preventDefault(); 34171 }); 34172 34173 if (settings.menu && !settings.ariaHideMenu) { 34174 self.aria('haspopup', true); 34175 } 34176 }, 34177 34178 /** 34179 * Returns true/false if the menuitem has sub menu. 34180 * 34181 * @method hasMenus 34182 * @return {Boolean} True/false state if it has submenu. 34183 */ 34184 hasMenus: function() { 34185 return !!this.settings.menu; 34186 }, 34187 34188 /** 34189 * Shows the menu for the menu item. 34190 * 34191 * @method showMenu 34192 */ 34193 showMenu: function() { 34194 var self = this, settings = self.settings, menu, parent = self.parent(); 34195 34196 parent.items().each(function(ctrl) { 34197 if (ctrl !== self) { 34198 ctrl.hideMenu(); 34199 } 34200 }); 34201 34202 if (settings.menu) { 34203 menu = self.menu; 34204 34205 if (!menu) { 34206 menu = settings.menu; 34207 34208 // Is menu array then auto constuct menu control 34209 if (menu.length) { 34210 menu = { 34211 type: 'menu', 34212 items: menu 34213 }; 34214 } else { 34215 menu.type = menu.type || 'menu'; 34216 } 34217 34218 if (parent.settings.itemDefaults) { 34219 menu.itemDefaults = parent.settings.itemDefaults; 34220 } 34221 34222 menu = self.menu = Factory.create(menu).parent(self).renderTo(); 34223 menu.reflow(); 34224 menu.fire('show'); 34225 menu.on('cancel', function(e) { 34226 e.stopPropagation(); 34227 self.focus(); 34228 menu.hide(); 34229 }); 34230 34231 menu.on('hide', function(e) { 34232 if (e.control === menu) { 34233 self.removeClass('selected'); 34234 } 34235 }); 34236 34237 menu.submenu = true; 34238 } else { 34239 menu.show(); 34240 } 34241 34242 menu._parentMenu = parent; 34243 34244 menu.addClass('menu-sub'); 34245 34246 var rel = menu.testMoveRel( 34247 self.getEl(), 34248 self.isRtl() ? ['tl-tr', 'bl-br', 'tr-tl', 'br-bl'] : ['tr-tl', 'br-bl', 'tl-tr', 'bl-br'] 34249 ); 34250 34251 menu.moveRel(self.getEl(), rel); 34252 menu.rel = rel; 34253 34254 rel = 'menu-sub-' + rel; 34255 menu.removeClass(menu._lastRel); 34256 menu.addClass(rel); 34257 menu._lastRel = rel; 34258 34259 self.addClass('selected'); 34260 self.aria('expanded', true); 34261 } 34262 }, 34263 34264 /** 34265 * Hides the menu for the menu item. 34266 * 34267 * @method hideMenu 34268 */ 34269 hideMenu: function() { 34270 var self = this; 34271 34272 if (self.menu) { 34273 self.menu.items().each(function(item) { 34274 if (item.hideMenu) { 34275 item.hideMenu(); 34276 } 34277 }); 34278 34279 self.menu.hide(); 34280 self.aria('expanded', false); 34281 } 34282 34283 return self; 34284 }, 34285 34286 /** 34287 * Renders the control as a HTML string. 34288 * 34289 * @method renderHtml 34290 * @return {String} HTML representing the control. 34291 */ 34292 renderHtml: function() { 34293 var self = this, id = self._id, settings = self.settings, prefix = self.classPrefix, text = self.encode(self._text); 34294 var icon = self.settings.icon, image = '', shortcut = settings.shortcut; 34295 34296 if (icon) { 34297 self.parent().addClass('menu-has-icons'); 34298 } 34299 34300 if (settings.image) { 34301 icon = 'none'; 34302 image = ' style="background-image: url(\'' + settings.image + '\')"'; 34303 } 34304 34305 if (shortcut && Env.mac) { 34306 // format shortcut for Mac 34307 shortcut = shortcut.replace(/ctrl\+alt\+/i, '⌥⌘'); // ctrl+cmd 34308 shortcut = shortcut.replace(/ctrl\+/i, '⌘'); // ctrl symbol 34309 shortcut = shortcut.replace(/alt\+/i, '⌥'); // cmd symbol 34310 shortcut = shortcut.replace(/shift\+/i, '⇧'); // shift symbol 34311 } 34312 34313 icon = prefix + 'ico ' + prefix + 'i-' + (self.settings.icon || 'none'); 34314 34315 return ( 34316 '<div id="' + id + '" class="' + self.classes() + '" tabindex="-1">' + 34317 (text !== '-' ? '<i class="' + icon + '"' + image + '></i>\u00a0' : '') + 34318 (text !== '-' ? '<span id="' + id + '-text" class="' + prefix + 'text">' + text + '</span>' : '') + 34319 (shortcut ? '<div id="' + id + '-shortcut" class="' + prefix + 'menu-shortcut">' + shortcut + '</div>' : '') + 34320 (settings.menu ? '<div class="' + prefix + 'caret"></div>' : '') + 34321 '</div>' 34322 ); 34323 }, 34324 34325 /** 34326 * Gets invoked after the control has been rendered. 34327 * 34328 * @method postRender 34329 */ 34330 postRender: function() { 34331 var self = this, settings = self.settings; 34332 34333 var textStyle = settings.textStyle; 34334 if (typeof(textStyle) == "function") { 34335 textStyle = textStyle.call(this); 34336 } 34337 34338 if (textStyle) { 34339 var textElm = self.getEl('text'); 34340 if (textElm) { 34341 textElm.setAttribute('style', textStyle); 34342 } 34343 } 34344 34345 self.on('mouseenter click', function(e) { 34346 if (e.control === self) { 34347 if (!settings.menu && e.type === 'click') { 34348 self.fire('select'); 34349 self.parent().hideAll(); 34350 } else { 34351 self.showMenu(); 34352 34353 if (e.aria) { 34354 self.menu.focus(true); 34355 } 34356 } 34357 } 34358 }); 34359 34360 self._super(); 34361 34362 return self; 34363 }, 34364 34365 active: function(state) { 34366 if (typeof(state) != "undefined") { 34367 this.aria('checked', state); 34368 } 34369 34370 return this._super(state); 34371 }, 34372 34373 /** 34374 * Removes the control and it's menus. 34375 * 34376 * @method remove 34377 */ 34378 remove: function() { 34379 this._super(); 34380 34381 if (this.menu) { 34382 this.menu.remove(); 34383 } 34384 } 34385 }); 34386 }); 34387 34388 // Included from: js/tinymce/classes/ui/Menu.js 34389 34390 /** 34391 * Menu.js 34392 * 34393 * Copyright, Moxiecode Systems AB 34394 * Released under LGPL License. 34395 * 34396 * License: http://www.tinymce.com/license 34397 * Contributing: http://www.tinymce.com/contributing 34398 */ 34399 34400 /** 34401 * Creates a new menu. 34402 * 34403 * @-x-less Menu.less 34404 * @class tinymce.ui.Menu 34405 * @extends tinymce.ui.FloatPanel 34406 */ 34407 define("tinymce/ui/Menu", [ 34408 "tinymce/ui/FloatPanel", 34409 "tinymce/ui/MenuItem", 34410 "tinymce/util/Tools" 34411 ], function(FloatPanel, MenuItem, Tools) { 34412 "use strict"; 34413 34414 var Menu = FloatPanel.extend({ 34415 Defaults: { 34416 defaultType: 'menuitem', 34417 border: 1, 34418 layout: 'stack', 34419 role: 'application', 34420 bodyRole: 'menu', 34421 ariaRoot: true 34422 }, 34423 34424 /** 34425 * Constructs a instance with the specified settings. 34426 * 34427 * @constructor 34428 * @param {Object} settings Name/value object with settings. 34429 */ 34430 init: function(settings) { 34431 var self = this; 34432 34433 settings.autohide = true; 34434 settings.constrainToViewport = true; 34435 34436 if (settings.itemDefaults) { 34437 var items = settings.items, i = items.length; 34438 34439 while (i--) { 34440 items[i] = Tools.extend({}, settings.itemDefaults, items[i]); 34441 } 34442 } 34443 34444 self._super(settings); 34445 self.addClass('menu'); 34446 }, 34447 34448 /** 34449 * Repaints the control after a layout operation. 34450 * 34451 * @method repaint 34452 */ 34453 repaint: function() { 34454 this.toggleClass('menu-align', true); 34455 34456 this._super(); 34457 34458 this.getEl().style.height = ''; 34459 this.getEl('body').style.height = ''; 34460 34461 return this; 34462 }, 34463 34464 /** 34465 * Hides/closes the menu. 34466 * 34467 * @method cancel 34468 */ 34469 cancel: function() { 34470 var self = this; 34471 34472 self.hideAll(); 34473 self.fire('select'); 34474 }, 34475 34476 /** 34477 * Hide menu and all sub menus. 34478 * 34479 * @method hideAll 34480 */ 34481 hideAll: function() { 34482 var self = this; 34483 34484 this.find('menuitem').exec('hideMenu'); 34485 34486 return self._super(); 34487 }, 34488 /* 34489 getContainerElm: function() { 34490 var doc = document, id = this.classPrefix + 'menucontainer'; 34491 34492 var elm = doc.getElementById(id); 34493 if (!elm) { 34494 elm = doc.createElement('div'); 34495 elm.id = id; 34496 elm.setAttribute('role', 'application'); 34497 elm.className = this.classPrefix + '-reset'; 34498 elm.style.position = 'absolute'; 34499 elm.style.top = elm.style.left = '0'; 34500 elm.style.overflow = 'visible'; 34501 doc.body.appendChild(elm); 34502 } 34503 34504 return elm; 34505 }, 34506 */ 34507 /** 34508 * Invoked before the menu is rendered. 34509 * 34510 * @method preRender 34511 */ 34512 preRender: function() { 34513 var self = this; 34514 34515 self.items().each(function(ctrl) { 34516 var settings = ctrl.settings; 34517 34518 if (settings.icon || settings.selectable) { 34519 self._hasIcons = true; 34520 return false; 34521 } 34522 }); 34523 34524 return self._super(); 34525 } 34526 }); 34527 34528 return Menu; 34529 }); 34530 34531 // Included from: js/tinymce/classes/ui/Radio.js 34532 34533 /** 34534 * Radio.js 34535 * 34536 * Copyright, Moxiecode Systems AB 34537 * Released under LGPL License. 34538 * 34539 * License: http://www.tinymce.com/license 34540 * Contributing: http://www.tinymce.com/contributing 34541 */ 34542 34543 /** 34544 * Creates a new radio button. 34545 * 34546 * @-x-less Radio.less 34547 * @class tinymce.ui.Radio 34548 * @extends tinymce.ui.Checkbox 34549 */ 34550 define("tinymce/ui/Radio", [ 34551 "tinymce/ui/Checkbox" 34552 ], function(Checkbox) { 34553 "use strict"; 34554 34555 return Checkbox.extend({ 34556 Defaults: { 34557 classes: "radio", 34558 role: "radio" 34559 } 34560 }); 34561 }); 34562 34563 // Included from: js/tinymce/classes/ui/ResizeHandle.js 34564 34565 /** 34566 * ResizeHandle.js 34567 * 34568 * Copyright, Moxiecode Systems AB 34569 * Released under LGPL License. 34570 * 34571 * License: http://www.tinymce.com/license 34572 * Contributing: http://www.tinymce.com/contributing 34573 */ 34574 34575 /** 34576 * Renders a resize handle that fires ResizeStart, Resize and ResizeEnd events. 34577 * 34578 * @-x-less ResizeHandle.less 34579 * @class tinymce.ui.ResizeHandle 34580 * @extends tinymce.ui.Widget 34581 */ 34582 define("tinymce/ui/ResizeHandle", [ 34583 "tinymce/ui/Widget", 34584 "tinymce/ui/DragHelper" 34585 ], function(Widget, DragHelper) { 34586 "use strict"; 34587 34588 return Widget.extend({ 34589 /** 34590 * Renders the control as a HTML string. 34591 * 34592 * @method renderHtml 34593 * @return {String} HTML representing the control. 34594 */ 34595 renderHtml: function() { 34596 var self = this, prefix = self.classPrefix; 34597 34598 self.addClass('resizehandle'); 34599 34600 if (self.settings.direction == "both") { 34601 self.addClass('resizehandle-both'); 34602 } 34603 34604 self.canFocus = false; 34605 34606 return ( 34607 '<div id="' + self._id + '" class="' + self.classes() + '">' + 34608 '<i class="' + prefix + 'ico ' + prefix + 'i-resize"></i>' + 34609 '</div>' 34610 ); 34611 }, 34612 34613 /** 34614 * Called after the control has been rendered. 34615 * 34616 * @method postRender 34617 */ 34618 postRender: function() { 34619 var self = this; 34620 34621 self._super(); 34622 34623 self.resizeDragHelper = new DragHelper(this._id, { 34624 start: function() { 34625 self.fire('ResizeStart'); 34626 }, 34627 34628 drag: function(e) { 34629 if (self.settings.direction != "both") { 34630 e.deltaX = 0; 34631 } 34632 34633 self.fire('Resize', e); 34634 }, 34635 34636 stop: function() { 34637 self.fire('ResizeEnd'); 34638 } 34639 }); 34640 }, 34641 34642 remove: function() { 34643 if (this.resizeDragHelper) { 34644 this.resizeDragHelper.destroy(); 34645 } 34646 34647 return this._super(); 34648 } 34649 }); 34650 }); 34651 34652 // Included from: js/tinymce/classes/ui/Spacer.js 34653 34654 /** 34655 * Spacer.js 34656 * 34657 * Copyright, Moxiecode Systems AB 34658 * Released under LGPL License. 34659 * 34660 * License: http://www.tinymce.com/license 34661 * Contributing: http://www.tinymce.com/contributing 34662 */ 34663 34664 /** 34665 * Creates a spacer. This control is used in flex layouts for example. 34666 * 34667 * @-x-less Spacer.less 34668 * @class tinymce.ui.Spacer 34669 * @extends tinymce.ui.Widget 34670 */ 34671 define("tinymce/ui/Spacer", [ 34672 "tinymce/ui/Widget" 34673 ], function(Widget) { 34674 "use strict"; 34675 34676 return Widget.extend({ 34677 /** 34678 * Renders the control as a HTML string. 34679 * 34680 * @method renderHtml 34681 * @return {String} HTML representing the control. 34682 */ 34683 renderHtml: function() { 34684 var self = this; 34685 34686 self.addClass('spacer'); 34687 self.canFocus = false; 34688 34689 return '<div id="' + self._id + '" class="' + self.classes() + '"></div>'; 34690 } 34691 }); 34692 }); 34693 34694 // Included from: js/tinymce/classes/ui/SplitButton.js 34695 34696 /** 34697 * SplitButton.js 34698 * 34699 * Copyright, Moxiecode Systems AB 34700 * Released under LGPL License. 34701 * 34702 * License: http://www.tinymce.com/license 34703 * Contributing: http://www.tinymce.com/contributing 34704 */ 34705 34706 /** 34707 * Creates a split button. 34708 * 34709 * @-x-less SplitButton.less 34710 * @class tinymce.ui.SplitButton 34711 * @extends tinymce.ui.Button 34712 */ 34713 define("tinymce/ui/SplitButton", [ 34714 "tinymce/ui/MenuButton", 34715 "tinymce/ui/DomUtils" 34716 ], function(MenuButton, DomUtils) { 34717 return MenuButton.extend({ 34718 Defaults: { 34719 classes: "widget btn splitbtn", 34720 role: "button" 34721 }, 34722 34723 /** 34724 * Repaints the control after a layout operation. 34725 * 34726 * @method repaint 34727 */ 34728 repaint: function() { 34729 var self = this, elm = self.getEl(), rect = self.layoutRect(), mainButtonElm, menuButtonElm; 34730 34731 self._super(); 34732 34733 mainButtonElm = elm.firstChild; 34734 menuButtonElm = elm.lastChild; 34735 34736 DomUtils.css(mainButtonElm, { 34737 width: rect.w - DomUtils.getSize(menuButtonElm).width, 34738 height: rect.h - 2 34739 }); 34740 34741 DomUtils.css(menuButtonElm, { 34742 height: rect.h - 2 34743 }); 34744 34745 return self; 34746 }, 34747 34748 /** 34749 * Sets the active menu state. 34750 * 34751 * @private 34752 */ 34753 activeMenu: function(state) { 34754 var self = this; 34755 34756 DomUtils.toggleClass(self.getEl().lastChild, self.classPrefix + 'active', state); 34757 }, 34758 34759 /** 34760 * Renders the control as a HTML string. 34761 * 34762 * @method renderHtml 34763 * @return {String} HTML representing the control. 34764 */ 34765 renderHtml: function() { 34766 var self = this, id = self._id, prefix = self.classPrefix; 34767 var icon = self.settings.icon ? prefix + 'ico ' + prefix + 'i-' + self.settings.icon : ''; 34768 34769 return ( 34770 '<div id="' + id + '" class="' + self.classes() + '" role="button" tabindex="-1">' + 34771 '<button type="button" hidefocus="1" tabindex="-1">' + 34772 (icon ? '<i class="' + icon + '"></i>' : '') + 34773 (self._text ? (icon ? ' ' : '') + self._text : '') + 34774 '</button>' + 34775 '<button type="button" class="' + prefix + 'open" hidefocus="1" tabindex="-1">' + 34776 //(icon ? '<i class="' + icon + '"></i>' : '') + 34777 (self._menuBtnText ? (icon ? '\u00a0' : '') + self._menuBtnText : '') + 34778 ' <i class="' + prefix + 'caret"></i>' + 34779 '</button>' + 34780 '</div>' 34781 ); 34782 }, 34783 34784 /** 34785 * Called after the control has been rendered. 34786 * 34787 * @method postRender 34788 */ 34789 postRender: function() { 34790 var self = this, onClickHandler = self.settings.onclick; 34791 34792 self.on('click', function(e) { 34793 var node = e.target; 34794 34795 if (e.control == this) { 34796 // Find clicks that is on the main button 34797 while (node) { 34798 if ((e.aria && e.aria.key != 'down') || (node.nodeName == 'BUTTON' && node.className.indexOf('open') == -1)) { 34799 e.stopImmediatePropagation(); 34800 onClickHandler.call(this, e); 34801 return; 34802 } 34803 34804 node = node.parentNode; 34805 } 34806 } 34807 }); 34808 34809 delete self.settings.onclick; 34810 34811 return self._super(); 34812 } 34813 }); 34814 }); 34815 34816 // Included from: js/tinymce/classes/ui/StackLayout.js 34817 34818 /** 34819 * StackLayout.js 34820 * 34821 * Copyright, Moxiecode Systems AB 34822 * Released under LGPL License. 34823 * 34824 * License: http://www.tinymce.com/license 34825 * Contributing: http://www.tinymce.com/contributing 34826 */ 34827 34828 /** 34829 * This layout uses the browsers layout when the items are blocks. 34830 * 34831 * @-x-less StackLayout.less 34832 * @class tinymce.ui.StackLayout 34833 * @extends tinymce.ui.FlowLayout 34834 */ 34835 define("tinymce/ui/StackLayout", [ 34836 "tinymce/ui/FlowLayout" 34837 ], function(FlowLayout) { 34838 "use strict"; 34839 34840 return FlowLayout.extend({ 34841 Defaults: { 34842 containerClass: 'stack-layout', 34843 controlClass: 'stack-layout-item', 34844 endClass : 'break' 34845 } 34846 }); 34847 }); 34848 34849 // Included from: js/tinymce/classes/ui/TabPanel.js 34850 34851 /** 34852 * TabPanel.js 34853 * 34854 * Copyright, Moxiecode Systems AB 34855 * Released under LGPL License. 34856 * 34857 * License: http://www.tinymce.com/license 34858 * Contributing: http://www.tinymce.com/contributing 34859 */ 34860 34861 /** 34862 * Creates a tab panel control. 34863 * 34864 * @-x-less TabPanel.less 34865 * @class tinymce.ui.TabPanel 34866 * @extends tinymce.ui.Panel 34867 * 34868 * @setting {Number} activeTab Active tab index. 34869 */ 34870 define("tinymce/ui/TabPanel", [ 34871 "tinymce/ui/Panel", 34872 "tinymce/ui/DomUtils" 34873 ], function(Panel, DomUtils) { 34874 "use strict"; 34875 34876 return Panel.extend({ 34877 lastIdx: 0, 34878 34879 Defaults: { 34880 layout: 'absolute', 34881 defaults: { 34882 type: 'panel' 34883 } 34884 }, 34885 34886 /** 34887 * Activates the specified tab by index. 34888 * 34889 * @method activateTab 34890 * @param {Number} idx Index of the tab to activate. 34891 */ 34892 activateTab: function(idx) { 34893 var activeTabElm; 34894 34895 if (this.activeTabId) { 34896 activeTabElm = this.getEl(this.activeTabId); 34897 DomUtils.removeClass(activeTabElm, this.classPrefix + 'active'); 34898 activeTabElm.setAttribute('aria-selected', "false"); 34899 } 34900 34901 this.activeTabId = 't' + idx; 34902 34903 activeTabElm = this.getEl('t' + idx); 34904 activeTabElm.setAttribute('aria-selected', "true"); 34905 DomUtils.addClass(activeTabElm, this.classPrefix + 'active'); 34906 34907 if (idx != this.lastIdx) { 34908 this.items()[this.lastIdx].hide(); 34909 this.lastIdx = idx; 34910 } 34911 34912 this.items()[idx].show().fire('showtab'); 34913 this.reflow(); 34914 }, 34915 34916 /** 34917 * Renders the control as a HTML string. 34918 * 34919 * @method renderHtml 34920 * @return {String} HTML representing the control. 34921 */ 34922 renderHtml: function() { 34923 var self = this, layout = self._layout, tabsHtml = '', prefix = self.classPrefix; 34924 34925 self.preRender(); 34926 layout.preRender(self); 34927 34928 self.items().each(function(ctrl, i) { 34929 var id = self._id + '-t' + i; 34930 34931 ctrl.aria('role', 'tabpanel'); 34932 ctrl.aria('labelledby', id); 34933 34934 tabsHtml += ( 34935 '<div id="' + id + '" class="' + prefix + 'tab" ' + 34936 'unselectable="on" role="tab" aria-controls="' + ctrl._id + '" aria-selected="false" tabIndex="-1">' + 34937 self.encode(ctrl.settings.title) + 34938 '</div>' 34939 ); 34940 }); 34941 34942 return ( 34943 '<div id="' + self._id + '" class="' + self.classes() + '" hidefocus="1" tabindex="-1">' + 34944 '<div id="' + self._id + '-head" class="' + prefix + 'tabs" role="tablist">' + 34945 tabsHtml + 34946 '</div>' + 34947 '<div id="' + self._id + '-body" class="' + self.classes('body') + '">' + 34948 layout.renderHtml(self) + 34949 '</div>' + 34950 '</div>' 34951 ); 34952 }, 34953 34954 /** 34955 * Called after the control has been rendered. 34956 * 34957 * @method postRender 34958 */ 34959 postRender: function() { 34960 var self = this; 34961 34962 self._super(); 34963 34964 self.settings.activeTab = self.settings.activeTab || 0; 34965 self.activateTab(self.settings.activeTab); 34966 34967 this.on('click', function(e) { 34968 var targetParent = e.target.parentNode; 34969 34970 if (e.target.parentNode.id == self._id + '-head') { 34971 var i = targetParent.childNodes.length; 34972 34973 while (i--) { 34974 if (targetParent.childNodes[i] == e.target) { 34975 self.activateTab(i); 34976 } 34977 } 34978 } 34979 }); 34980 }, 34981 34982 /** 34983 * Initializes the current controls layout rect. 34984 * This will be executed by the layout managers to determine the 34985 * default minWidth/minHeight etc. 34986 * 34987 * @method initLayoutRect 34988 * @return {Object} Layout rect instance. 34989 */ 34990 initLayoutRect: function() { 34991 var self = this, rect, minW, minH; 34992 34993 minW = DomUtils.getSize(self.getEl('head')).width; 34994 minW = minW < 0 ? 0 : minW; 34995 minH = 0; 34996 self.items().each(function(item, i) { 34997 minW = Math.max(minW, item.layoutRect().minW); 34998 minH = Math.max(minH, item.layoutRect().minH); 34999 if (self.settings.activeTab != i) { 35000 item.hide(); 35001 } 35002 }); 35003 35004 self.items().each(function(ctrl) { 35005 ctrl.settings.x = 0; 35006 ctrl.settings.y = 0; 35007 ctrl.settings.w = minW; 35008 ctrl.settings.h = minH; 35009 35010 ctrl.layoutRect({ 35011 x: 0, 35012 y: 0, 35013 w: minW, 35014 h: minH 35015 }); 35016 }); 35017 35018 var headH = DomUtils.getSize(self.getEl('head')).height; 35019 35020 self.settings.minWidth = minW; 35021 self.settings.minHeight = minH + headH; 35022 35023 rect = self._super(); 35024 rect.deltaH += headH; 35025 rect.innerH = rect.h - rect.deltaH; 35026 35027 return rect; 35028 } 35029 }); 35030 }); 35031 35032 // Included from: js/tinymce/classes/ui/TextBox.js 35033 35034 /** 35035 * TextBox.js 35036 * 35037 * Copyright, Moxiecode Systems AB 35038 * Released under LGPL License. 35039 * 35040 * License: http://www.tinymce.com/license 35041 * Contributing: http://www.tinymce.com/contributing 35042 */ 35043 35044 /** 35045 * Creates a new textbox. 35046 * 35047 * @-x-less TextBox.less 35048 * @class tinymce.ui.TextBox 35049 * @extends tinymce.ui.Widget 35050 */ 35051 define("tinymce/ui/TextBox", [ 35052 "tinymce/ui/Widget", 35053 "tinymce/ui/DomUtils" 35054 ], function(Widget, DomUtils) { 35055 "use strict"; 35056 35057 return Widget.extend({ 35058 /** 35059 * Constructs a instance with the specified settings. 35060 * 35061 * @constructor 35062 * @param {Object} settings Name/value object with settings. 35063 * @setting {Boolean} multiline True if the textbox is a multiline control. 35064 * @setting {Number} maxLength Max length for the textbox. 35065 * @setting {Number} size Size of the textbox in characters. 35066 */ 35067 init: function(settings) { 35068 var self = this; 35069 35070 self._super(settings); 35071 35072 self._value = settings.value || ''; 35073 self.addClass('textbox'); 35074 35075 if (settings.multiline) { 35076 self.addClass('multiline'); 35077 } else { 35078 // TODO: Rework this 35079 self.on('keydown', function(e) { 35080 if (e.keyCode == 13) { 35081 self.parents().reverse().each(function(ctrl) { 35082 e.preventDefault(); 35083 35084 if (ctrl.hasEventListeners('submit') && ctrl.toJSON) { 35085 ctrl.fire('submit', {data: ctrl.toJSON()}); 35086 return false; 35087 } 35088 }); 35089 } 35090 }); 35091 } 35092 }, 35093 35094 /** 35095 * Getter/setter function for the disabled state. 35096 * 35097 * @method value 35098 * @param {Boolean} [state] State to be set. 35099 * @return {Boolean|tinymce.ui.ComboBox} True/false or self if it's a set operation. 35100 */ 35101 disabled: function(state) { 35102 var self = this; 35103 35104 if (self._rendered && typeof(state) != 'undefined') { 35105 self.getEl().disabled = state; 35106 } 35107 35108 return self._super(state); 35109 }, 35110 35111 /** 35112 * Getter/setter function for the control value. 35113 * 35114 * @method value 35115 * @param {String} [value] Value to be set. 35116 * @return {String|tinymce.ui.ComboBox} Value or self if it's a set operation. 35117 */ 35118 value: function(value) { 35119 var self = this; 35120 35121 if (typeof(value) != "undefined") { 35122 self._value = value; 35123 35124 if (self._rendered) { 35125 self.getEl().value = value; 35126 } 35127 35128 return self; 35129 } 35130 35131 if (self._rendered) { 35132 return self.getEl().value; 35133 } 35134 35135 return self._value; 35136 }, 35137 35138 /** 35139 * Repaints the control after a layout operation. 35140 * 35141 * @method repaint 35142 */ 35143 repaint: function() { 35144 var self = this, style, rect, borderBox, borderW = 0, borderH = 0, lastRepaintRect; 35145 35146 style = self.getEl().style; 35147 rect = self._layoutRect; 35148 lastRepaintRect = self._lastRepaintRect || {}; 35149 35150 // Detect old IE 7+8 add lineHeight to align caret vertically in the middle 35151 var doc = document; 35152 if (!self.settings.multiline && doc.all && (!doc.documentMode || doc.documentMode <= 8)) { 35153 style.lineHeight = (rect.h - borderH) + 'px'; 35154 } 35155 35156 borderBox = self._borderBox; 35157 borderW = borderBox.left + borderBox.right + 8; 35158 borderH = borderBox.top + borderBox.bottom + (self.settings.multiline ? 8 : 0); 35159 35160 if (rect.x !== lastRepaintRect.x) { 35161 style.left = rect.x + 'px'; 35162 lastRepaintRect.x = rect.x; 35163 } 35164 35165 if (rect.y !== lastRepaintRect.y) { 35166 style.top = rect.y + 'px'; 35167 lastRepaintRect.y = rect.y; 35168 } 35169 35170 if (rect.w !== lastRepaintRect.w) { 35171 style.width = (rect.w - borderW) + 'px'; 35172 lastRepaintRect.w = rect.w; 35173 } 35174 35175 if (rect.h !== lastRepaintRect.h) { 35176 style.height = (rect.h - borderH) + 'px'; 35177 lastRepaintRect.h = rect.h; 35178 } 35179 35180 self._lastRepaintRect = lastRepaintRect; 35181 self.fire('repaint', {}, false); 35182 35183 return self; 35184 }, 35185 35186 /** 35187 * Renders the control as a HTML string. 35188 * 35189 * @method renderHtml 35190 * @return {String} HTML representing the control. 35191 */ 35192 renderHtml: function() { 35193 var self = this, id = self._id, settings = self.settings, value = self.encode(self._value, false), extraAttrs = ''; 35194 35195 if ("spellcheck" in settings) { 35196 extraAttrs += ' spellcheck="' + settings.spellcheck + '"'; 35197 } 35198 35199 if (settings.maxLength) { 35200 extraAttrs += ' maxlength="' + settings.maxLength + '"'; 35201 } 35202 35203 if (settings.size) { 35204 extraAttrs += ' size="' + settings.size + '"'; 35205 } 35206 35207 if (settings.subtype) { 35208 extraAttrs += ' type="' + settings.subtype + '"'; 35209 } 35210 35211 if (self.disabled()) { 35212 extraAttrs += ' disabled="disabled"'; 35213 } 35214 35215 if (settings.multiline) { 35216 return ( 35217 '<textarea id="' + id + '" class="' + self.classes() + '" ' + 35218 (settings.rows ? ' rows="' + settings.rows + '"' : '') + 35219 ' hidefocus="1"' + extraAttrs + '>' + value + 35220 '</textarea>' 35221 ); 35222 } 35223 35224 return '<input id="' + id + '" class="' + self.classes() + '" value="' + value + '" hidefocus="1"' + extraAttrs + ' />'; 35225 }, 35226 35227 /** 35228 * Called after the control has been rendered. 35229 * 35230 * @method postRender 35231 */ 35232 postRender: function() { 35233 var self = this; 35234 35235 DomUtils.on(self.getEl(), 'change', function(e) { 35236 self.fire('change', e); 35237 }); 35238 35239 return self._super(); 35240 }, 35241 35242 remove: function() { 35243 DomUtils.off(this.getEl()); 35244 this._super(); 35245 } 35246 }); 35247 }); 35248 35249 // Included from: js/tinymce/classes/ui/Throbber.js 35250 35251 /** 35252 * Throbber.js 35253 * 35254 * Copyright, Moxiecode Systems AB 35255 * Released under LGPL License. 35256 * 35257 * License: http://www.tinymce.com/license 35258 * Contributing: http://www.tinymce.com/contributing 35259 */ 35260 35261 /** 35262 * This class enables you to display a Throbber for any element. 35263 * 35264 * @-x-less Throbber.less 35265 * @class tinymce.ui.Throbber 35266 */ 35267 define("tinymce/ui/Throbber", [ 35268 "tinymce/ui/DomUtils", 35269 "tinymce/ui/Control" 35270 ], function(DomUtils, Control) { 35271 "use strict"; 35272 35273 /** 35274 * Constructs a new throbber. 35275 * 35276 * @constructor 35277 * @param {Element} elm DOM Html element to display throbber in. 35278 * @param {Boolean} inline Optional true/false state if the throbber should be appended to end of element for infinite scroll. 35279 */ 35280 return function(elm, inline) { 35281 var self = this, state, classPrefix = Control.classPrefix; 35282 35283 /** 35284 * Shows the throbber. 35285 * 35286 * @method show 35287 * @param {Number} [time] Time to wait before showing. 35288 * @return {tinymce.ui.Throbber} Current throbber instance. 35289 */ 35290 self.show = function(time) { 35291 self.hide(); 35292 35293 state = true; 35294 35295 window.setTimeout(function() { 35296 if (state) { 35297 elm.appendChild(DomUtils.createFragment( 35298 '<div class="' + classPrefix + 'throbber' + (inline ? ' ' + classPrefix + 'throbber-inline' : '') + '"></div>' 35299 )); 35300 } 35301 }, time || 0); 35302 35303 return self; 35304 }; 35305 35306 /** 35307 * Hides the throbber. 35308 * 35309 * @method hide 35310 * @return {tinymce.ui.Throbber} Current throbber instance. 35311 */ 35312 self.hide = function() { 35313 var child = elm.lastChild; 35314 35315 if (child && child.className.indexOf('throbber') != -1) { 35316 child.parentNode.removeChild(child); 35317 } 35318 35319 state = false; 35320 35321 return self; 35322 }; 35323 }; 35324 }); 35325 35326 expose(["tinymce/dom/EventUtils","tinymce/dom/Sizzle","tinymce/dom/DomQuery","tinymce/html/Styles","tinymce/dom/TreeWalker","tinymce/util/Tools","tinymce/dom/Range","tinymce/html/Entities","tinymce/Env","tinymce/dom/DOMUtils","tinymce/dom/ScriptLoader","tinymce/AddOnManager","tinymce/html/Node","tinymce/html/Schema","tinymce/html/SaxParser","tinymce/html/DomParser","tinymce/html/Writer","tinymce/html/Serializer","tinymce/dom/Serializer","tinymce/dom/TridentSelection","tinymce/util/VK","tinymce/dom/ControlSelection","tinymce/dom/Selection","tinymce/Formatter","tinymce/UndoManager","tinymce/EnterKey","tinymce/ForceBlocks","tinymce/EditorCommands","tinymce/util/URI","tinymce/util/Class","tinymce/util/EventDispatcher","tinymce/ui/Selector","tinymce/ui/Collection","tinymce/ui/DomUtils","tinymce/ui/Control","tinymce/ui/Factory","tinymce/ui/KeyboardNavigation","tinymce/ui/Container","tinymce/ui/DragHelper","tinymce/ui/Scrollable","tinymce/ui/Panel","tinymce/ui/Movable","tinymce/ui/Resizable","tinymce/ui/FloatPanel","tinymce/ui/Window","tinymce/ui/MessageBox","tinymce/WindowManager","tinymce/util/Quirks","tinymce/util/Observable","tinymce/EditorObservable","tinymce/Shortcuts","tinymce/Editor","tinymce/util/I18n","tinymce/FocusManager","tinymce/EditorManager","tinymce/LegacyInput","tinymce/util/XHR","tinymce/util/JSON","tinymce/util/JSONRequest","tinymce/util/JSONP","tinymce/util/LocalStorage","tinymce/Compat","tinymce/ui/Layout","tinymce/ui/AbsoluteLayout","tinymce/ui/Tooltip","tinymce/ui/Widget","tinymce/ui/Button","tinymce/ui/ButtonGroup","tinymce/ui/Checkbox","tinymce/ui/PanelButton","tinymce/ui/ColorButton","tinymce/ui/ComboBox","tinymce/ui/Path","tinymce/ui/ElementPath","tinymce/ui/FormItem","tinymce/ui/Form","tinymce/ui/FieldSet","tinymce/ui/FilePicker","tinymce/ui/FitLayout","tinymce/ui/FlexLayout","tinymce/ui/FlowLayout","tinymce/ui/FormatControls","tinymce/ui/GridLayout","tinymce/ui/Iframe","tinymce/ui/Label","tinymce/ui/Toolbar","tinymce/ui/MenuBar","tinymce/ui/MenuButton","tinymce/ui/ListBox","tinymce/ui/MenuItem","tinymce/ui/Menu","tinymce/ui/Radio","tinymce/ui/ResizeHandle","tinymce/ui/Spacer","tinymce/ui/SplitButton","tinymce/ui/StackLayout","tinymce/ui/TabPanel","tinymce/ui/TextBox","tinymce/ui/Throbber"]); 35327 })(this);